Why this note exists
To connect implementation choices to business trust instead of leaving them as isolated technical details.
Editorial Note
A practical guide to NestJS modules, controllers, providers, DTOs, dependency injection, validation, guards, middleware, interceptors, and exception handling.

Why this note exists
To connect implementation choices to business trust instead of leaving them as isolated technical details.
Reading pace
9 minutes focused on judgment, not filler.
Best paired with
A case study or project conversation when you want the thinking behind the build, not just the output.
NestJS is a structured Node.js framework for building server-side applications with TypeScript. It adds modules, dependency injection, decorators, validation, and testing conventions to the familiar Express or Fastify request model.
This tutorial builds the core mental model of a NestJS REST API: a request reaches a controller, the controller delegates to a service, the service accesses a repository or database, and the result returns through the same layers. We will also place middleware, guards, pipes, interceptors, and exception filters correctly.
npm install -g @nestjs/cli | nest new task-api | cd task-api | npm run start:dev
The generated application includes AppModule, AppController, and AppService. These demonstrate the three fundamental roles in NestJS. A module defines a feature boundary, a controller owns HTTP routes, and a service implements reusable business logic.
nest generate module tasks | nest generate controller tasks | nest generate service tasks
TasksModule groups the feature and registers its controller and providers. TasksController translates HTTP input into application calls. TasksService coordinates business rules, repositories, and external services. Keep controllers thin: receive input, call one service operation, and return the result.
HTTP request → controller → service → repository/database → service → response
The controller knows HTTP concepts such as route parameters, query strings, request bodies, and status codes. The service should not depend on those transport details. It receives typed values, performs the use case, and calls a repository. This makes the same logic reusable from background jobs, scheduled tasks, message consumers, and tests.
@Get() findAll(@Query('status') status?: string) | @Get(':id') findOne(@Param('id', ParseIntPipe) id: number) | @Post() create(@Body() dto: CreateTaskDto) | @Put(':id') update(@Param('id', ParseIntPipe) id: number, @Body() dto: UpdateTaskDto)
Use @Query() for filters and pagination, @Param() for path parameters, and @Body() for JSON payloads. ParseIntPipe converts the id to a number and rejects invalid input before the controller executes. Inject TasksService through the controller constructor.
npm install class-validator class-transformer @nestjs/mapped-types
CreateTaskDto can define title with @IsString() and @MinLength(3), plus an optional status validated with @IsIn(['open', 'done']). Define UpdateTaskDto by extending PartialType(CreateTaskDto). PartialType makes creation fields optional while preserving validation metadata.
Enable a global ValidationPipe in main.ts with transform: true, whitelist: true, and forbidNonWhitelisted: true. Transform converts supported primitive values, whitelist removes undeclared properties, and forbidNonWhitelisted rejects unexpected fields.
A class decorated with @Injectable() can be registered as a provider. Nest creates the instance and supplies it through constructor injection. TasksService can inject TasksRepository, while TasksController injects TasksService. The classes depend on collaborators instead of constructing them manually.
@Injectable() export class TasksService { constructor(private readonly repository: TasksRepository) {} }
Dependency injection reduces coupling and simplifies testing. A test module can replace TasksRepository with a mock provider without changing the service. Providers can also represent database clients, configuration, caches, mailers, queues, or external integrations.
Middleware runs early for logging, correlation IDs, cookies, or request preprocessing. Guards decide whether a request may continue, typically for authentication, roles, or permissions. Pipes validate and transform controller arguments before the handler runs. Interceptors wrap execution for timing, response mapping, serialization, caching, or observability.
Throw Nest's built-in HTTP exceptions from the layer that understands the failure. A service can throw NotFoundException when a record does not exist, BadRequestException for an invalid operation, or ConflictException for a uniqueness conflict. Nest converts these into structured HTTP responses.
Use a custom exception filter when the entire API needs one error envelope, database errors must be translated, or failures require centralized logging. Avoid repetitive try/catch blocks in every controller because they obscure the successful path and tend to produce inconsistent responses.
middleware → guards → interceptor before → pipes → controller → service → interceptor after → exception filter on failure
This sequence is the practical map for deciding where behavior belongs. Authorization goes in a guard, validation in a pipe, request timing in an interceptor, and error formatting in an exception filter.
Connect TasksRepository to PostgreSQL through Prisma or TypeORM, add pagination to GET /tasks, protect mutation routes with an authentication guard, and test TasksService by mocking its repository. The project will then have the boundaries of a maintainable production API rather than a collection of route handlers.
The central NestJS principle is straightforward: controllers handle HTTP, services express use cases, providers encapsulate dependencies, and framework primitives enforce behavior around the request. Keeping those responsibilities distinct allows the application to remain understandable as features and teams grow.