Controllers and Providers
Controllers with @Controller()/@Get()/@Post(), providers with @Injectable(), and constructor-based dependency injection.
Controllers handle incoming requests
A controller is a class responsible for a set of related routes. The @Controller() decorator marks the class as one and optionally sets a path prefix shared by every route inside it; individual methods are then mapped to HTTP verbs with @Get(), @Post(), @Put(), @Patch(), and @Delete().
import { Controller, Get, Post, Body, Param } from '@nestjs/common';
@Controller('users')
export class UsersController {
@Get()
findAll() {
return [{ id: 1, name: 'Ada Lovelace' }];
}
@Get(':id')
findOne(@Param('id') id: string) {
return { id, name: 'Ada Lovelace' };
}
@Post()
create(@Body() body: { name: string }) {
return { id: 2, name: body.name };
}
}
@Param('id') and @Body() are parameter decorators — they tell Nest exactly which part of the incoming request to hand to that argument, so the method never has to reach into a raw req object. Nest serializes whatever you return to JSON automatically, and uses 201 Created by default for @Post() handlers.
Providers hold the actual logic
A controller should stay thin — parsing input and shaping output — while the real business logic (data access, calculations, calls to other services) lives in a provider, most commonly a service. Any class marked @Injectable() is a provider Nest's DI container can manage and hand out to whatever needs it.
import { Injectable } from '@nestjs/common';
interface User {
id: number;
name: string;
}
@Injectable()
export class UsersService {
private users: User[] = [{ id: 1, name: 'Ada Lovelace' }];
findAll(): User[] {
return this.users;
}
findOne(id: number): User | undefined {
return this.users.find((user) => user.id === id);
}
create(name: string): User {
const user = { id: this.users.length + 1, name };
this.users.push(user);
return user;
}
}
Constructor-based dependency injection
A controller obtains a provider not by constructing it itself, but by declaring it as a constructor parameter — Nest's DI container instantiates UsersService (once, as a singleton by default) and passes it in automatically:
import { Controller, Get, Post, Body, Param, NotFoundException } from '@nestjs/common';
import { UsersService } from './users.service';
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Get()
findAll() {
return this.usersService.findAll();
}
@Get(':id')
findOne(@Param('id') id: string) {
const user = this.usersService.findOne(Number(id));
if (!user) {
throw new NotFoundException(`User ${id} not found`);
}
return user;
}
@Post()
create(@Body() body: { name: string }) {
return this.usersService.create(body.name);
}
}
private readonly usersService: UsersService is TypeScript's parameter property shorthand — it both declares usersService as a field on the class and assigns it, in one line. Nest reads the parameter's type (UsersService) via reflect-metadata to know which provider to inject; this is exactly why the emitDecoratorMetadata compiler option from the previous page is required.
Throwing NotFoundException (or any of Nest's other built-in HTTP exceptions — BadRequestException, ForbiddenException, UnauthorizedException, and so on) is the idiomatic way to return an error response; Nest catches it and turns it into the correct status code and a JSON error body automatically.
Common mistakes
- Forgetting
@Injectable()on a service class — Nest's DI container can only manage classes explicitly marked as providers, and injecting an unmarked class fails at startup with a dependency resolution error. - Putting business logic directly in the controller instead of delegating to a service — it works for a toy example, but makes the logic untestable in isolation and unusable from anywhere except that one controller.
- Forgetting that a provider must also be registered in its module's
providersarray (covered next) — declaring@Injectable()alone doesn't make Nest aware the class exists.