InteractiveFrameworks

Controllers

Read route and query parameters, accept a JSON body, await an async service, and override the status code Nest picks for a POST.

What you'll learn

  • Declare routes whose paths compose from the controller prefix and the method path
  • Read route parameters, the query string and the JSON body with parameter decorators, and treat them as the strings they are
  • Write an async handler that awaits a service, and override a POST's default 201 with @HttpCode()

A controller turns HTTP requests into method calls and return values into responses. Everything about the mapping is declared with decorators: the ones on the class and its methods describe the routes, and the ones on the parameters describe where each argument comes from. The handler itself is an ordinary method, which is what makes it easy to read and easy to test.

Route paths compose

@Controller('users') sets a prefix for every route in the class, and each method decorator adds to it:

@Controller('users')
export class UsersController {
  @Get()            // GET /users
  findAll() {}

  @Get(':id')       // GET /users/42
  findOne() {}

  @Post()           // POST /users
  create() {}
}

A segment starting with : is a route parameter. :id matches exactly one path segment, whatever it contains, and makes the value available under the name id. There is a method decorator for each verb: @Get, @Post, @Put, @Patch, @Delete, and @All for any of them.

Reading parts of the request

Nest hands request data to a handler through parameter decorators. Each one names a part of the request, and optionally a key inside it:

DecoratorGives you
@Param('id')one route parameter, as a string
@Param()every route parameter, as an object
@Query('page')one value from the query string, ?page=2
@Query()the whole query string, as an object
@Body()the parsed JSON body
@Headers('authorization')one request header
@Req()the raw request, when nothing above fits
@Get(':id')
findOne(@Param('id') id: string, @Query('fields') fields?: string) {
  return { id, fields: fields ? fields.split(',') : [] };
}

GET /users/42?fields=name,email calls findOne('42', 'name,email'). Route parameters arrive URL-decoded, so /users/Ada%20Lovelace gives 'Ada Lovelace'.

Everything in a URL is a string

id above is typed string because it is one: 42 in a URL is the characters 4 and 2. The same goes for the query string. ?shout=true gives the string 'true', and ?shout=false gives the string 'false', which is not false: it is a non-empty string, and non-empty strings are truthy. A check like if (shout) treats both the same. Compare with the string you mean, shout === 'true', or convert. Lesson 5 shows how pipes convert route parameters into numbers before the handler runs; for now, do it by hand.

Bodies

@Post() routes usually carry a body. Nest parses application/json before the handler runs, so @Body() is already an object:

@Post()
create(@Body() body: { name: string }) {
  return { created: body.name };
}

A body that is not valid JSON never reaches the handler; Nest answers 400 Bad Request itself.

Status codes

A handler that returns normally answers 200 OK, except @Post() handlers, which answer 201 Created, on the assumption that a POST creates something. When that assumption is wrong, say so with @HttpCode():

@Post('search')
@HttpCode(200)
search(@Body() query: { term: string }) { ... }

Decorators stack, and the order of @Post('search') and @HttpCode(200) does not matter. Note that @HttpCode(201) on a POST changes nothing, because 201 is already the default. It is the kind of line that looks like it does something, and it is worth recognising.

Handlers can be async

Most real handlers wait for something: a database, another service, a file. Declare the handler async and return a Promise; Nest awaits it and sends the resolved value. The rule to remember is that whatever you return is what the client gets. Return an object containing a Promise you forgot to await, and the client gets {} for it, because a Promise has no JSON representation. TypeScript catches this when the handler declares its return type, which is a good reason to declare it.

Your task

AppService.greet(name) is asynchronous, as a lookup usually is. Add two routes to AppController:

  1. GET /greet/:name answers { greeting } with the service's greeting for that name. When the query string carries shout=true, the greeting is upper-cased.
  2. POST /echo answers { received } with the JSON body it was sent, with status 200.

When the tests pass, use the request bar in the Endpoints tab: send /greet/you with and without ?shout=true, and send /echo a body of your own.

When it fails

  • {"greeting":{}}: the handler returned the service's Promise without await. Make the handler async and await the call.
  • Hello, undefined!: the parameter has no @Param('name') decorator, so Nest passed nothing.
  • Status 201 on /echo: the default for a POST. @HttpCode(200) overrides it.
  • Hello, Ada! when you expected shouting on ?shout=true, or shouting on ?shout=false: re-read the section on strings.

Remember

  • Routes compose: the controller's prefix plus the method's path.
  • Parameter decorators say where each argument comes from; without one, an argument is undefined.
  • Everything in a URL is a string until something converts it.
  • POST answers 201 by default; @HttpCode() changes it. Async handlers are awaited, and an unawaited Promise serialises as {}.
Stuck? Show a hint

Route parameters come through @Param('name') and query values through @Query('shout'), both as strings, so compare shout with the string 'true'. The service's greet() returns a Promise: make the handler async and await it. For /echo, @Post('echo') plus @Body(), and @HttpCode(200) because nothing is created.