This episode covers the controller as the layer that handles HTTP requests: creating controllers and route handlers, using the GET POST PUT DELETE PATCH method decorators, reading route parameters, query parameters, and body, plus setting status codes and serialization.

A controller is the gateway for every request in a NestJS application. In episode 4 we focus on how routes are mapped, how data from the URL and body is read, and how the response is controlled.
A strong understanding of controllers will determine the quality of your REST API. After this episode, you'll be able to build clean, consistent CRUD endpoints.
The fastest way to create a controller is through the generator:
nest g controller productsThis command creates the products.controller.ts file in the src/products folder and automatically registers it in the module.
The @Controller decorator accepts a path prefix:
import { Controller, Get } from "@nestjs/common";
@Controller("products")
export class ProductsController {
@Get()
findAll(): string[] {
return ["Kaos", "Celana", "Sepatu"];
}
}With @Controller("products"), every route in this controller is prefixed with /products. The findAll method with @Get() responds to GET /products.
NestJS provides a decorator for every HTTP method:
@Controller("products")
export class ProductsController {
@Get()
findAll(): string[] {
return ["Kaos", "Celana"];
}
@Post()
create(): string {
return "Produk dibuat";
}
@Put(":id")
update(@Param("id") id: string): string {
return `Produk ${id} diperbarui`;
}
@Delete(":id")
remove(@Param("id") id: string): string {
return `Produk ${id} dihapus`;
}
@Patch(":id")
partialUpdate(@Param("id") id: string): string {
return `Produk ${id} ditambal`;
}
}Each of the @Get, @Post, @Put, @Delete, and @Patch decorators maps an HTTP method to a specific method.
Route parameters allow dynamic values in the URL:
@Get(":id")
findOne(@Param("id") id: string): string {
return `Produk dengan id ${id}`;
}Requesting GET /products/123 will fill id with the value 123. The :id route must be declared in the decorator path.
Query parameters appear after the question mark in a URL:
@Get()
findAll(@Query("limit") limit: string): string {
return `Membatasi hasil sebanyak ${limit}`;
}Requesting GET /products?limit=10 will fill limit with 10. @Query can also accept the entire query object without arguments.
For methods that send data in the body, use @Body:
@Post()
create(@Body() body: { name: string; price: number }): string {
return `Produk ${body.name} dengan harga ${body.price}`;
}NestJS automatically parses the JSON body and injects it into the parameter. Later on, the body is better validated using a DTO — we'll cover that in episode 7.
NestJS provides default status codes: 200 for GET, 201 for POST. You can change them with the @HttpCode decorator:
import { Controller, Post, HttpCode, HttpStatus } from "@nestjs/common";
@Controller("products")
export class ProductsController {
@Post()
@HttpCode(HttpStatus.CREATED)
create(): string {
return "Produk dibuat";
}
}@HttpCode(HttpStatus.CREATED) uses the HttpStatus enum from @nestjs/common so the code isn't hard-coded.
NestJS automatically serializes the value returned by a controller into JSON. You can return an object, array, or string — all are handled automatically. For finer control (for example, hiding fields), we'll cover interceptors and class-transformer in episodes 7 and 18.
Besides @HttpCode, you can use @Header for custom headers or @Res for full control over the Express response:
import { Controller, Get, Res } from "@nestjs/common";
import { Response } from "express";
@Controller("products")
export class ProductsController {
@Get()
findAll(@Res() res: Response): void {
res.status(200).json(["Kaos", "Celana"]);
}
}Using @Res gives full control, but you have to manage the response manually. It's recommended to use NestJS's default approach and only use @Res when truly necessary.
Episode 4 equips you with the full controller and routing capabilities: HTTP method decorators, reading parameters from the URL, query, and body, plus controlling status codes and serialization.
Key takeaways:
@Controller("path") sets the route prefix for all methods.@Get, @Post, @Put, @Delete, @Patch map HTTP methods.@Param reads values from the path, @Query from the query string.@Body parses JSON from the request body.@HttpCode sets the status code; the default is 200/201.In the next episode 5 we'll discuss providers and dependency injection — creating service providers, injecting them into controllers, understanding singleton and request-scoped scopes, custom providers, factory providers, aliases, and how to share providers between modules.