Chapter 03 — Controllers (Handling Requests)

Chapter 03 — Controllers (Handling Requests)

Hey everyone! Welcome back to Namaste NestJS! 🙏

Today we meet the receptionist of your app — the Controller. Its only job is to answer the door: "Which URL was requested? Which HTTP method? What data came along?" — and then hand the work to someone else. Controllers are the easiest building block to love, so let's dive in.

What we will cover:

  • What a controller is (and what it should NOT do)
  • Routing with @Controller and HTTP method decorators
  • Reading route params with @Param
  • Reading query strings with @Query
  • Reading the request body with @Body
  • Status codes, headers, and redirects
  • Sub-routes and route wildcards
  • Common mistakes beginners make
  • Interview Questions

1. What is a Controller?

A controller is a class whose methods handle incoming HTTP requests and return a response. Nothing more.

┌─────────────────────────────────────────────────────────────┐
│                   WHAT A CONTROLLER DOES                   │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   ✅ Decide WHICH method runs for a URL + HTTP verb         │
│   ✅ Pull data out of the request (params, query, body)     │
│   ✅ Call a SERVICE to do the real work                     │
│   ✅ Return a result (Nest turns it into a response)        │
│                                                             │
│   ❌ It should NOT contain business logic                   │
│   ❌ It should NOT talk to the database directly            │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Think of it like this: a receptionist takes your request and routes you to the right department. A good receptionist never does your paperwork themselves — they hand it to the chef (the service). Keep controllers thin.


2. Routing — @Controller and HTTP Methods

The @Controller('cats') decorator gives every route in the class a common prefix. Then each method gets an HTTP-method decorator.

import { Controller, Get, Post } from '@nestjs/common';

@Controller('cats')            // 👈 route PREFIX → everything here starts with /cats
export class CatsController {

  @Get()                       // GET  /cats
  findAll() {
    return 'this returns all cats';
  }

  @Post()                      // POST /cats
  create() {
    return 'this creates a cat';
  }
}

The full URL is always prefix + method path:

@Controller('cats')  +  @Get('breeds')   ──→   GET /cats/breeds
        │                     │
     prefix               method path

Here are the HTTP-method decorators you'll use every day:

DecoratorHTTP VerbTypical Use
@Get()GETRead / fetch data
@Post()POSTCreate a new resource
@Put()PUTReplace a resource fully
@Patch()PATCHUpdate part of a resource
@Delete()DELETERemove a resource

3. Route Params — @Param

A route param is a dynamic piece of the URL, like the 42 in /cats/42. You mark it with :name and read it with @Param.

import { Controller, Get, Param } from '@nestjs/common';

@Controller('cats')
export class CatsController {

  @Get(':id')                          // 👈 :id is a placeholder
  findOne(@Param('id') id: string) {   // 👈 pull just the "id" out
    return `this returns cat #${id}`;
  }
}

Let's trace a real request by hand:

HAND TRACE:  GET /cats/42
──────────────────────────
  1. Nest matches @Controller('cats') + @Get(':id')
  2. It sees the URL segment "42" lines up with ":id"
  3. @Param('id') extracts   id = "42"   (always a STRING!)
  4. findOne("42") runs
                     ▼
     Response: "this returns cat #42"  ✔

Important: route params are always strings. If you need a number, convert it (or use a Pipe — we'll automate that in Chapter 07). You can also grab the whole params object with @Param() (no argument).


4. Query Strings — @Query

A query string is the part after the ? — used for filtering, sorting, and pagination. Read it with @Query.

import { Controller, Get, Query } from '@nestjs/common';

@Controller('cats')
export class CatsController {

  // GET /cats?limit=10&breed=persian
  @Get()
  findAll(
    @Query('limit') limit: string,
    @Query('breed') breed: string,
  ) {
    return `first ${limit} cats of breed ${breed}`;
  }
}
PARAM vs QUERY — don't mix them up:
────────────────────────────────────
  /cats/42            → 42 is a PARAM   (identifies ONE resource)
  /cats?limit=10      → limit is a QUERY (filters/options)

5. Request Body — @Body

For POST/PUT/PATCH, the client sends data in the body (usually JSON). Read it with @Body.

import { Controller, Post, Body } from '@nestjs/common';

@Controller('cats')
export class CatsController {

  @Post()
  create(@Body() body: { name: string; age: number }) {
    // body = the parsed JSON the client sent
    return `created a cat named ${body.name}, age ${body.age}`;
  }
}

Q: How does Nest parse the JSON automatically?

A: Under the hood, Express's body-parser is enabled by default, so incoming JSON is already parsed into a plain object by the time @Body() reads it. You don't wire anything up.

Pro tip: That inline type { name: string; age: number } works, but the professional way is a DTO (Data Transfer Object) — a dedicated class describing the body. DTOs unlock automatic validation, and we give them a whole chapter (Ch 06). For now, just know the body arrives as an object.


6. The Request Decorators Cheat Sheet

You've now seen the three you'll use 90% of the time. Here's the full family:

DecoratorReadsExample URL / source
@Param('id')A route param/cats/42
@Query('limit')A query field/cats?limit=10
@Body()The request bodyJSON payload
@Headers('auth')A request headerAuthorization: ...
@Req()The whole request objectescape hatch (use sparingly)

Note: avoid reaching for @Req() (the raw Express request) unless you truly need it. Using the specific decorators keeps your code readable and framework-agnostic.


7. Status Codes, Headers & Redirects

By default Nest returns 200 for everything except @Post, which returns 201. Override when needed:

import { Controller, Post, HttpCode, Header, Redirect } from '@nestjs/common';

@Controller('cats')
export class CatsController {

  @Post()
  @HttpCode(204)                       // 👈 force a 204 No Content
  create() {
    return;
  }

  @Get('docs')
  @Redirect('https://docs.nestjs.com', 301)  // 👈 redirect
  docs() {}

  @Get('report')
  @Header('Cache-Control', 'no-store')  // 👈 set a response header
  report() {
    return 'fresh report';
  }
}

In simple words: you rarely touch the raw response object. Decorators like @HttpCode, @Header, and @Redirect declare your intent, and Nest does the plumbing.


8. Little Traps Beginners Fall Into

┌─────────────────────────────────────────────────────────────┐
│   COMMON MISTAKES ❌                                         │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  Putting business logic in the controller.                 │
│     → Keep it thin. Delegate to a service.                 │
│                                                             │
│  Expecting @Param('id') to be a number.                    │
│     → It's a STRING. Convert it (or use ParseIntPipe).     │
│                                                             │
│  Route order matters: @Get('active') must come BEFORE      │
│  @Get(':id'), or "active" gets captured as an :id.         │
│                                                             │
│  Forgetting the controller must be listed in a module's    │
│  `controllers: []` array, or its routes won't exist.       │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Interview Questions — Quick Fire!

Q: What is a controller in NestJS?

"A controller is a class decorated with @Controller that handles incoming HTTP requests and returns responses. Its methods are mapped to routes using decorators like @Get and @Post. A controller should stay thin — it extracts data from the request and delegates the actual work to a service."

Q: How do you read a route parameter, a query string, and the body?

"You use parameter decorators: @Param to read route params like the id in /cats/:id, @Query to read query-string values like ?limit=10, and @Body to read the request payload. Each can take a key to extract a single field, or no argument to get the whole object."

Q: What HTTP status codes does Nest return by default?

"By default it returns 200 OK for most handlers and 201 Created for POST handlers. You can override this with the @HttpCode decorator, for example @HttpCode(204) for a no-content response."

Q: Why does route order matter?

"Nest matches routes in the order they're declared. A specific path like @Get('active') must be declared before a dynamic one like @Get(':id'), otherwise 'active' would be captured as the id parameter and the specific handler would never run."

Q: Should you use @Req() to access the request?

"Generally no. Nest provides dedicated decorators like @Param, @Query, and @Body that are cleaner and keep you decoupled from the underlying Express or Fastify object. @Req is an escape hatch for the rare case you need something those decorators don't expose."


Key Points to Remember

ConceptKey Takeaway
ControllerHandles routes; stays thin; delegates to services.
@Controller('x')Sets a route prefix for the whole class.
HTTP decorators@Get, @Post, @Put, @Patch, @Delete map methods to verbs.
@Param / @Query / @BodyExtract route params, query fields, and the payload.
Params are stringsConvert to numbers yourself (or via a pipe).
Route orderSpecific routes before dynamic :id routes.

What's Next?

Controllers keep pointing to a "service" that does the real work — but where do services come from, and how does Nest hand them to the controller without us calling new? That's Dependency Injection, the beating heart of Nest. In Chapter 04 we cover Providers & DI, and the "magic" finally makes sense.

Keep coding, keep shipping! See you in the next one!