Learn how NestJS controllers work. Routes, @Get and @Post, route params, query params, request body, status codes and a full CRUD example.
Node.js, NestJS, Java Backend Engineer
Every web app has the same basic job. A request comes in, something happens, a response goes out. In NestJS, the thing that sits at the front door and handles that is called a controller.
If you've just set up your first Nest project and you're wondering where your routes are supposed to go, this is the right place. We'll build a small cats controller step by step, and by the end you'll know how to handle GET and POST requests, read URL parameters, accept data from a client, and control what you send back.
A controller receives incoming requests and sends responses back to the client. That's the job.
NestJS has a routing system that decides which controller should handle which request. One controller can have many routes, and each route can do something different. For example, one route might return a list of cats, and another might add a new cat.
Under the hood, you build a controller with a class and some decorators. Decorators are those @Something() labels you put above classes and methods. They attach extra information to your code, and NestJS uses that information to build a map of which request goes where.
Here's the simplest version:
import { Controller, Get } from '@nestjs/common';@Controller('cats')export class CatsController { @Get() findAll(): string { return 'This action returns all cats'; }}Let's break this controller down.
@Controller('cats') marks the class as a controller. The cats part is an optional path prefix. It means every route inside this class starts with /cats, so you don't have to repeat it.@Get() tells Nest that the method below handles GET requests.findAll() is the method that runs. The name is up to you. Nest doesn't care what you call it.So when someone sends GET /cats, Nest runs findAll() and sends back the text.
Want a different path? Add one to the method decorator. @Get('breed') would give you GET /cats/breed. Nest simply joins the controller prefix and the method path together.
Tip: You don't have to type all this by hand. Run nest g controller cats and the CLI creates the file for you.What does NestJS send back by default? Return a value from your method and NestJS takes care of the rest.
This is the recommended way to work, and it's the one you'll use most of the time.
Next, let's add a route to create a cat:
import { Controller, Get, Post } from '@nestjs/common';@Controller('cats')export class CatsController { @Post() create(): string { return 'This action adds a new cat'; } @Get() findAll(): string { return 'This action returns all cats'; }}Nest has a decorator for each standard HTTP method: @Get(), @Post(), @Put(), @Delete(), @Patch(), @Options() and @Head(). There's also @All(), which handles every method on one route.
Sometimes the route needs to carry data. Think GET /cats/1 to fetch the cat with ID 1. You do this by adding a token to the path, like :id, and then reading it with @Param():
@Get(':id')findOne(@Param('id') id: string): string { return `This action returns a #${id} cat`;}Whatever the client puts in that spot of the URL ends up in the id variable. Don't forget to import Param from @nestjs/common.
One thing to check: route order Put your fixed routes before your parameter routes. Look at this:
@Controller('users')export class UsersController { @Get(':id') findOne() {} @Get('me') // never reached findMe() {}}A request to /users/me gets caught by :id first, because NestJS registers routes in the order you write them. Nothing crashes and there's no warning. The wrong handler just runs. Swap the order and you're fine. The docs also mention that Nest v12 adds opt-in settings to catch this problem (routeConflictPolicy) or sort routes by how specific they are (routeResolutionStrategy). Simply writing static routes first avoids the whole problem, though.
Query parameters are the bits after the question mark in a URL, like GET /cats?age=2&breed=Persian. Grab them with @Query():
@Get()async findAll(@Query('age') age: number, @Query('breed') breed: string) { return `This action returns all cats filtered by age: ${age} and breed: ${breed}`;}Here's a gotcha that trips up a lot of beginners. Query values always arrive as strings. Writing age: number in TypeScript does not convert anything. If you need a real number, use a pipe such as ParseIntPipe.
For a POST request, the client usually sends data in the body. Use @Body() to read it. But first you need to describe what that data looks like. That description is called a DTO, short for Data Transfer Object.
// create-cat.dto.tsexport class CreateCatDto { name: string; age: number; breed: string;}Then use it in the controller:
@Post()async create(@Body() createCatDto: CreateCatDto) { return 'This action adds a new cat';}Why a class and not an interface? Interfaces disappear when TypeScript compiles to JavaScript, so Nest can't see them at runtime. Classes stick around. That matters because features like pipes need to read the type information while the app is running. So use classes for DTOs.
Need something other than the default response? A few small decorators cover most cases.
Custom status code:
@Post()@HttpCode(204)create() { return 'This action adds a new cat';}Custom header:
@Post()@Header('Cache-Control', 'no-store')create() { return 'This action adds a new cat';}Redirect:
@Get()@Redirect('https://nestjs.com', 301)If you don't give @Redirect() a status code, it uses 302. You can also return an object like { url: '...' } from the method to change the destination on the fly.
All of these (HttpCode, Header, Redirect) come from @nestjs/common.
Real apps talk to databases, and that means waiting. Nest handles this without any extra work. Mark the method async and return your data:
@Get()async findAll(): Promise<any[]> { return [];}NestJS waits for the promise to finish and sends the result. It also works with RxJS observables, if you happen to use them.
Put it all together and you get a complete controller for creating, reading, updating and deleting cats:
import { Controller, Get, Query, Post, Body, Put, Param, Delete } from '@nestjs/common';import { CreateCatDto, UpdateCatDto, ListAllEntities } from './dto.js';@Controller('cats')export class CatsController { @Post() create(@Body() createCatDto: CreateCatDto) { return 'This action adds a new cat'; } @Get() findAll(@Query() query: ListAllEntities) { return `This action returns all cats (limit: ${query.limit} items)`; } @Get(':id') findOne(@Param('id') id: string) { return `This action returns a #${id} cat`; } @Put(':id') update(@Param('id') id: string, @Body() updateCatDto: UpdateCatDto) { return `This action updates a #${id} cat`; } @Delete(':id') remove(@Param('id') id: string) { return `This action removes a #${id} cat`; }}Shortcut: Run nest g resource cats and the CLI generates a CRUD controller with validation already wired up.This is the step people forget. Writing the controller isn't enough. Nest doesn't know it exists until you add it to a module:
import { Module } from '@nestjs/common';import { CatsController } from './cats/cats.controller.js';@Module({ controllers: [CatsController],})export class AppModule {}If your new routes return 404, check this first.
Sometimes you need the underlying request or response object from Express. You can inject them with @Req() and @Res():
@Post()create(@Res() res: Response) { res.status(HttpStatus.CREATED).send();}This works, but there are catches:
@Res(), you are responsible for sending the response. If you forget, the request just hangs.@HttpCode() and @Header() decorators.If you only need the response object to set a cookie or a header, use passthrough mode:
@Get()findAll(@Res({ passthrough: true }) res: Response) { res.status(HttpStatus.OK); return [];}Now you can touch the response and still let Nest handle the rest. For most apps, the standard approach is simpler and better.
Running npm run start:dev on your laptop is fine for learning. At some point, though, you'll want your API online where other people can reach it.
Because Nest runs on Node.js, you need a server where you can install Node and keep a process running all the time. That's why a VPS makes more sense here than basic shared hosting, which is usually built for PHP sites and doesn't give you that kind of control.
I use Verpex VPS Hosting for this. Their unmanaged Linux VPS gives you full root access, so you pick the operating system, install the Node version you want, and set up the stack yourself. The plans run on NVMe SSD storage with dedicated resources (so you're not sharing your slice with a noisy neighbour), and there's a 99.9% uptime SLA plus round-the-clock infrastructure support. A small plan is plenty for a first NestJS project, and you can move up later if your traffic grows.
One honest warning: "unmanaged" means exactly that. Updates, firewall rules and security are your job, not theirs. If you've never touched a Linux server before, expect a bit of a learning curve. If you're comfortable with SSH and a terminal, that freedom is the whole point.
A typical first deploy looks like this: connect over SSH, install Node.js, clone your project, run npm install and npm run build, then keep it alive with a process manager like PM2.
Disclosure: this is an affiliate link. If you sign up through it, I may earn a commission at no extra cost to you.
| What you want | Decorator |
|---|---|
| Mark a class as a controller | @Controller('path') |
| Handle GET, POST, PUT, DELETE | @Get(), @Post(), @Put(), @Delete() |
| Read a URL parameter | @Param('id') |
A class that handles incoming requests for specific routes and returns responses to the client.
Run nest g controller name with the Nest CLI, or write a class by hand and add the @Controller() decorator. Then register it in a module's controllers array.
@Param() reads values that are part of the path, like the 1 in /cats/1. @Query() reads values after the question mark, like ?age=2.
Most often the controller isn't registered in a module. Also check that your path and HTTP method match, and that a parameter route like :id isn't sitting above a fixed route like me.
Your controller can now take requests and send responses, but real logic shouldn't live inside it. Keep controllers thin and move the actual work into providers (services). That's the next chapter to read. After that, look at modules, pipes for validating input, and exception filters for handling errors.
NestJS is a Node.js framework written in TypeScript that gives your backend a clear structure, so it doesn’t get messy as it grows. Think of it as organized scaffolding on top of Express (the default) or Fastify.
React is a front-end JavaScript library. React was developed by the Facebook Software Engineer Jordan Walke. React is a tool, for building UI components.
@Query('name') |
| Read the request body | @Body() |
| Read headers | @Headers('name') |
| Change the status code | @HttpCode(204) |
| Set a response header | @Header('key', 'value') |
| Redirect | @Redirect('url', 301) |