It would be really awesome if we could choose to expose our applications over both HTTP, TCP, or both without code modifications. This would be ideal as you could still expose your application to end users over HTTP, but also to backend consumers over TCP. The .NET framework ServiceStack is a great example of this sort of architecture. It encourages you to design your API in a way that can be consumed over arbitrary transports.
After briefly reading through the code, I think there are just a few overarching things standing in the way of this:
This would help unify the application and microservice interfaces (since microservices already expect you to return Observables
).
Taking this example from the docs:
@Get('/:id')
public async getUser(@Response() res, @Param('id') id) {
const user = await this.usersService.getUser(id);
res.status(HttpStatus.OK).json(user);
}
It would be nice if it could optionally be written as:
@Get('/:id')
public getUser(@Param('id') id): Promise<User> {
return this.usersService.getUser(id);
}
Controller methods should support returning HttpException
, Promise<T>
, Observable<T>
, and T
. All these types will automatically be serialized to the response independent of the underlying transport.
We will need a unified way of deserializing request parameters and passing them to controller methods that agnostic to the transport mechanism. To work around this I propose that we support class-based request definitions with sane, interoperable behavior by default that also allows the user to override the deserialization behavior across transports.
Continuing with our previous example from the docs, a controller might look something like this:
@Message()
class GetUserRequest {
@Param('id') // for HTTP use `req.params.id`
@MessageData('id') // for TCP use `data.id`
id: number;
// when no decorators are specified, the default behavior might be:
// HTTP: `req.params[key] || req.body[key] || req.query[key]`
// TCP: `data[key]`
group: string;
}
@Controller()
export class UsersController {
@Get('users/:id')
@MessagePattern({ cmd: 'get-user' })
public getUser(request: GetUserRequest): Promise<User> {
return this.usersService.getUser(request.id);
}
}
These require some more thinking, especially if we want to do it in a backwards-compatible way.
Hi @marshall007,
I think the big part of your ideas covers the new v4 release. I'm happy to share this with you finally. :cat:
Thanks!
Hey @kamilmysliwiec, the new v4 API looks great! Have you considered the second part of my proposal concerning class-based request DTOs? Taking this example from the docs:
// dto/create-cat.dto.ts
export class CreateCatDto {
readonly name: string;
readonly age: number;
readonly breed: string;
}
```ts
// cats.controller.ts
@Post()
async create(@Body() createCatDto: CreateCatDto) {
// TODO: Add some logic here
}
I would like to see support for using the **request decorators on class properties** like so:
```ts
// dto/create-cat.dto.ts
export class CreateUpdateCatDto {
@Param('cat_id')
readonly id?: string; // req.params.cat_id
@Body()
readonly name: string; // req.body.name
@Body()
readonly age: number; // req.body.age
@Body()
readonly breed: string; // req.body.breed
@Query('api_key')
@Header('X-Auth-Api-Key')
readonly apiKey: string; // req.query.api_key || req.headers['X-Auth-Api-Key']
}
Note how this allows us to specify fallbacks for plucking out various request options. In this case if the API key is not specified as a query param, we can still grab it from the header.
More importantly, these fallbacks would allow us to map incoming TCP and HTTP requests to the same DTO, which paves the way for totally transport-agnostic controller definitions.
// cats.controller.ts
@Post()
async create(@Message(CreateUpdateCatDto) req: CreateUpdateCatDto) {
// TODO: Add some logic here
}
@Put('/:cat_id')
async create(@Message(CreateUpdateCatDto) req: CreateUpdateCatDto) {
// TODO: Add some logic here
}
This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.
Most helpful comment
Hey @kamilmysliwiec, the new v4 API looks great! Have you considered the second part of my proposal concerning class-based request DTOs? Taking this example from the docs:
```ts
// cats.controller.ts
@Post()
async create(@Body() createCatDto: CreateCatDto) {
// TODO: Add some logic here
}
Note how this allows us to specify fallbacks for plucking out various request options. In this case if the API key is not specified as a query param, we can still grab it from the header.
More importantly, these fallbacks would allow us to map incoming TCP and HTTP requests to the same DTO, which paves the way for totally transport-agnostic controller definitions.