NestJS course Β· Module 1: NestJS Basics

Controllers - the centurions of the Empire

6 min read
In this lesson5

A messenger arrives at the camp gate with a request: GET /legion/5. The server received it - but who is to handle it? The application has dozens of methods and none of them knows this one is for it.

In a legion, distributing orders is the centurion's job. He stands between the messenger and the soldiers: he takes the order, recognises whom it concerns, passes it on and sends back the answer. He does not do the work himself - he directs it. In NestJS that centurion is a controller.

Declaring a controller

A controller is a class with a decorator saying which stretch of routes it handles:

1@Controller('legion')
2export class LegionController { }

The order is always the same: @Controller('legion'), then export class, and finally the class name with its body.

The 'legion' argument is a route prefix. Every method of this class will serve addresses beginning with /legion - and you do not have to repeat that on each of them. A controller with no argument, @Controller(), takes routes from the root.

Four verbs

Inside the controller, each method gets a decorator saying which request it answers:

1@Controller('tributes')
2export class TributesController {
3  @Get()
4  findAll() { }
5
6  @Post()
7  create() { }
8
9  @Put(':id')
10  update() { }
11
12  @Delete(':id')
13  remove() { }
14}

Four decorators match four actions on a resource. @Get() fetches, @Post() creates a new resource, @Put() updates an existing one, @Delete() removes it.

Telling @Post from @Put tends to confuse, so remember them by what repetition does: sending the same @Post twice creates two resources, while sending the same @Put twice leaves one, merely written twice.

The decorator's argument adds a piece of route. @Put(':id') in a controller prefixed tributes will serve /tributes/42. The colon marks a parameter - a slot any value falls into.

Three places data comes from

Since a route can contain a parameter, it has to be read somehow. Data arrives in a request by three roads, and each has its own decorator:

1@Get('search')
2search(@Query('province') province: string) { }
3
4@Get(':id')
5findOne(@Param('id') id: string) { }
6
7@Post()
8create(@Body() dto: CreateTributeDto) { }

@Param('id') pulls a value out of the address path - it is what reads 5 from /legion/5. @Query('province') takes a query parameter, what stands after the question mark: ?province=rome. @Body() reaches for the request body, sent with @Post and @Put.

Note the order of the first two methods. NestJS matches routes in the order they are declared, and :id matches any text - including the word search. If findOne came first, a GET /legion/search request would reach it with id equal to 'search'. That is why routes with a fixed segment are declared before routes with a parameter.

These three cover everything you usually need. Beware of two plausible-sounding names: @Path() does not exist in NestJS - the path is served by @Param. @Header() does exist but does something else: it sets a response header. To read request headers there is @Headers(), in the plural.

A controller method from the inside

Let's put it together. The order of a method's elements never varies:

1@Get()
2getAllLegionaries() {
3  return this.service.findAll();
4}

First the decorator @Get(), then the method name, then the body in braces, and inside it return this.service.findAll().

And here you see what a controller really is. This method contains no logic - it takes the request and hands it to a service. A centurion does not forge swords; he knows which smith to send to.

This is a rule I recommend keeping from day one: no database queries, calculations or business rules in a controller. When a controller method grows beyond a few lines, that is a sign it is doing something belonging to a service. The gain is practical - you will later call that same logic from a batch job or a queue consumer, where there is no HTTP at all.

The controller is not the request's first stop, either. On the way stand layers you will meet in later modules: first middleware, then guards (sentries that decide whether to let the request in), next interceptors (before the method is called) and pipes (they check and convert data, e.g. ValidationPipe). Only then does the controller method run, and its result goes back to the client through the interceptors.

Note that we do not build the response by hand. The returned value will be turned into JSON, and NestJS picks the status code itself: 200 for most methods, 201 for @Post, because something was created.

Summary

The centurion stands at the gate and knows whom to pass the order to:

  • a controller receives a request and directs it onward - it does not do the work itself,
  • declaration in order: @Controller('prefix'), export class, the class name,
  • the @Controller argument is a route prefix shared by every method of the class,
  • four verbs: @Get() fetches, @Post() creates a new resource, @Put() updates, @Delete() removes,
  • a repeated @Post creates two resources, a repeated @Put leaves one,
  • a colon in a route marks a parameter: @Put(':id') serves /tributes/42,
  • declare a fixed route (search) before a route with a parameter (:id), because the parameter would capture its address,
  • @Param('id') reads from the path, @Query('name') reads a query parameter after the ?, @Body() reads the request body,
  • @Path() does not exist; @Header() sets a response header, and request headers are read by @Headers(),
  • order inside a method: decorator, name, body, return this.service.findAll(),
  • no business logic in a controller - that way you can call it outside HTTP too,
  • NestJS picks the status code: 200, and 201 for @Post.

In the next lesson you will meet the ones the centurion passes orders to - services, where the real logic lives. For now remember: a controller recognises a request and names its executor - and every piece of data it needs arrives by one of three roads: the path, the query, or the body.

Code for this lesson: src/legion.controller.ts
1// Controllers - Centurions of the Imperium Structure
2import {
3  Controller, Get, Post, Put, Delete,
4  Body, Param, Query, HttpCode, Header,
5  ParseIntPipe, DefaultValuePipe,
6} from '@nestjs/common';
7
8console.log("NestJS Controllers - centurions receiving orders!");
9
10// ===========================================
11// 1. Basic controller with HTTP decorators
12// ===========================================
13
14interface Legionary {
15  id: number;
16  name: string;
17  rank: string;
18  experience: number;
19}
20
21@Controller('legiones')
22export class LegionController {
23  private legionaries: Legionary[] = [
24    { id: 1, name: 'Marcus Aurelius', rank: 'Centurio', experience: 10 },
25    { id: 2, name: 'Julia Domna', rank: 'Optio', experience: 8 },
26    { id: 3, name: 'Titus Flavius', rank: 'Miles', experience: 3 },
27  ];
28
29  // GET /legiones
30  @Get()
31  findAll(
32    @Query('rank') rank?: string,
33    @Query('limit', new DefaultValuePipe(10), ParseIntPipe) limit?: number,
34  ) {
35    let result = this.legionaries;
36    if (rank) {
37      result = result.filter(l => l.rank === rank);
38    }
39    return { legionaries: result.slice(0, limit), total: result.length };
40  }
41
42  // GET /legiones/:id
43  @Get(':id')
44  findOne(@Param('id', ParseIntPipe) id: number) {
45    const legionary = this.legionaries.find(l => l.id === id);
46    if (!legionary) {
47      return { error: 'Legionary non inventus!' };
48    }
49    return legionary;
50  }
51
52  // POST /legiones
53  @Post()
54  @HttpCode(201)
55  create(@Body() data: Omit<Legionary, 'id'>) {
56    const newLegionary: Legionary = {
57      id: Math.max(...this.legionaries.map(l => l.id)) + 1,
58      ...data,
59    };
60    this.legionaries.push(newLegionary);
61    return { message: 'Legionary recruited!', legionary: newLegionary };
62  }
63
64  // PUT /legiones/:id
65  @Put(':id')
66  update(@Param('id', ParseIntPipe) id: number, @Body() data: Partial<Legionary>) {
67    const index = this.legionaries.findIndex(l => l.id === id);
68    if (index === -1) return { error: 'Legionary non inventus!' };
69    this.legionaries[index] = { ...this.legionaries[index], ...data };
70    return { message: 'Data updated!', legionary: this.legionaries[index] };
71  }
72
73  // DELETE /legiones/:id
74  @Delete(':id')
75  @HttpCode(204)
76  remove(@Param('id', ParseIntPipe) id: number) {
77    this.legionaries = this.legionaries.filter(l => l.id !== id);
78  }
79
80  // GET /legiones/search/:name
81  @Get('search/:name')
82  @Header('X-Imperium', 'Roma-Aeterna')
83  search(@Param('name') name: string) {
84    return this.legionaries.filter(
85      l => l.name.toLowerCase().includes(name.toLowerCase())
86    );
87  }
88}
89
90console.log("\n=== PODSUMOWANIE KONTROLEROW ===");
91console.log("@Controller('prefix') - sets route prefix");
92console.log("@Get, @Post, @Put, @Delete - HTTP methods");
93console.log("@Param, @Query, @Body - extracting data from request");
94console.log("@HttpCode, @Header - response configuration");
95

Spotted a mistake in this lesson?

Check yourself

Answer the questions from this lesson. Pick an answer to see right away whether it is correct.

  1. 1. How do you extract a URL parameter in a NestJS controller (e.g., /legion/5)?

  2. 2. Which HTTP decorator in NestJS is used to create a new resource?

  3. 3. How do you get a query parameter (e.g., ?from=rome) in a NestJS controller?

Hands-on tasks in the game

  • Code editor

    Write a TributesController with the @Controller('tributes') decorator and four methods: findAll with @Get(), create with @Post(), update with @Put(':id'), and remove with @Delete(':id').

  • Vertical ordering

    Arrange the elements of the controller declaration in the correct order:

  • Click in order

    Arrange the elements of the controller GET method in the correct order:

  • Code editor

    Write a LegionController with the @Controller('legion') decorator and two GET endpoints: search with @Get('search'), which reads @Query('province') province and returns { province }, and findOne with @Get(':id'), which reads @Param('id') id and returns { id }. Declare search before :id, because the route with a parameter would capture the /legion/search address.

Useful articles