Introduzione
NestJS è oggi il framework backend Node.js più usato per costruire applicazioni server-side scalabili, tipizzate e manutenibili. Combina i concetti più solidi dell'ingegneria del software — Dependency Injection, moduli, architettura a strati — con la produttività di TypeScript e un ecosistema che copre praticamente ogni esigenza: REST, GraphQL, WebSocket, microservizi, CLI, cron job.
In questa guida costruiamo, passo per passo, una REST API completa e pronta per la produzione: un'API Blog con autenticazione JWT, refresh token, controllo degli accessi basato sui ruoli (RBAC), validazione, upload di file, logging centralizzato e gestione strutturata degli errori — esattamente lo stack che si trova dietro la maggior parte delle API NestJS reali nel 2026.
Cos'è NestJS
NestJS è un framework opinionated costruito sopra Express (o, in alternativa, Fastify) che impone una struttura precisa all'applicazione: ogni funzionalità è organizzata in moduli, ogni modulo espone controller (gestiscono le richieste HTTP) e provider (contengono la logica di business, tipicamente service), collegati tra loro tramite un sistema di Dependency Injection integrato e ispirato ad Angular.
Perché usarlo
- TypeScript nativo: tipizzazione end-to-end, autocompletamento, refactoring sicuro.
- Architettura imposta: a differenza di Express puro, NestJS obbliga a separare le responsabilità (controller/service/repository), riducendo il "big ball of mud" tipico dei progetti Node cresciuti senza struttura.
- Dependency Injection integrata: testabilità elevata, basso accoppiamento, provider intercambiabili (utile per mock nei test).
- Ecosistema maturo: moduli ufficiali per TypeORM, Prisma, Mongoose, Passport, GraphQL, WebSocket, Bull/BullMQ, Swagger, gRPC, microservizi.
- Decoratori dichiarativi: guard, interceptor, pipe ed exception filter permettono cross-cutting concerns (auth, logging, validazione) senza inquinare la logica di business.
Quando conviene (e quando no)
NestJS conviene quando il progetto ha una complessità che giustifica una struttura rigida: team di più sviluppatori, API destinate a crescere nel tempo, necessità di testabilità elevata, applicazioni enterprise con requisiti di sicurezza e compliance. È probabilmente eccessivo per uno script one-off o un prototipo che verrà buttato via in una settimana — lì Express puro o Fastify "nudo" restano più rapidi da avviare.
Vantaggi e Svantaggi
| Vantaggi | Svantaggi |
|---|---|
| Struttura chiara e consistente tra progetti diversi | Curva di apprendimento più ripida di Express puro (decoratori, DI, moduli) |
| Testabilità nativa grazie alla DI | Overhead di boilerplate per progetti molto piccoli |
| Ecosistema ufficiale ampio e ben mantenuto | Maggiore "magia" (decoratori, reflection) rispetto a codice esplicito |
| Ottima integrazione con TypeScript e Swagger/OpenAPI | Bundle size e cold start leggermente superiori a framework minimali (rilevante in ambienti serverless) |
| Facilita l'adozione di Clean Architecture / hexagonal | Richiede disciplina del team per non abusare della flessibilità dei moduli |
Casi Reali
NestJS è usato in produzione da aziende come Adidas, Roche, Autodesk e Decathlon, oltre a essere una scelta molto comune per backend SaaS B2B, piattaforme e-commerce, sistemi di gestione utenti e API di microservizi che devono comunicare tra loro tramite gRPC o code di messaggi (RabbitMQ, Kafka).
Prerequisiti
Prima di iniziare, assicurati di avere questi strumenti installati e di conoscere le basi di TypeScript (interfacce, decoratori, generics) e dei concetti REST (verbi HTTP, status code, idempotenza).
| Strumento | Versione consigliata | A cosa serve |
|---|---|---|
| Node.js | 20.x LTS o superiore | Runtime JavaScript su cui gira NestJS |
| npm | 10.x (incluso in Node 20) | Gestione dei pacchetti |
| NestJS CLI | @nestjs/cli 10.x+ | Scaffolding di moduli, controller, service |
| TypeScript | 5.x | Linguaggio in cui è scritto NestJS e la tua app |
| VS Code | Ultima stabile | Editor con supporto TypeScript nativo |
| PostgreSQL | 15.x o superiore | Database relazionale per l'esempio con TypeORM |
| Docker (opzionale) | 24.x+ | Avviare PostgreSQL localmente senza installazione nativa |
# Verifica le versioni installate
node -v
npm -v
# Installa la CLI di NestJS globalmente
npm install -g @nestjs/cli
nest --version
Suggerimento: se non vuoi installare PostgreSQL nativamente, avvialo con Docker:
docker run --name pg-blog -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=blog_db -p 5432:5432 -d postgres:16.
Architettura
Prima di scrivere codice, è fondamentale capire come NestJS instrada una richiesta HTTP attraverso i suoi componenti principali. Ecco il ciclo di vita completo di una richiesta:
Client
│
▼
┌─────────────┐
│ Middleware │ (es. logger, cookie-parser — eseguito prima del routing)
└──────┬───────┘
▼
┌─────────────┐
│ Guard │ (es. AuthGuard — decide se la richiesta può procedere)
└──────┬───────┘
▼
┌─────────────┐
│ Interceptor │ (fase "prima" — es. logging, trasformazione input)
└──────┬───────┘
▼
┌─────────────┐
│ Pipe │ (validazione e trasformazione dei parametri in ingresso)
└──────┬───────┘
▼
┌─────────────┐
│ Controller │ (riceve la richiesta, delega al service)
└──────┬───────┘
▼
┌─────────────┐
│ Service │ (logica di business, chiama i repository)
└──────┬───────┘
▼
┌─────────────┐
│ Repository / │ (accesso ai dati — TypeORM, Prisma, Mongoose...)
│ Database │
└──────┬───────┘
▼
┌─────────────┐
│ Interceptor │ (fase "dopo" — es. trasformazione della risposta)
└──────┬───────┘
▼
┌──────────────────┐
│ Exception Filter │ (intercetta SOLO se viene lanciata un'eccezione)
└──────┬────────────┘
▼
Response
Controller
Il livello più esterno: riceve le richieste HTTP, estrae parametri/body/query e delega immediatamente la logica al service corrispondente. Un controller non deve mai contenere logica di business — la sua unica responsabilità è il mapping HTTP ↔ chiamata di metodo.
Service
Contiene la logica di business vera e propria. È un provider iniettabile,
tipicamente marcato con @Injectable(), e viene iniettato nel controller (o in
altri service) tramite il costruttore.
Module
Il modulo è l'unità organizzativa di NestJS: raggruppa controller, provider e import di altri
moduli. Ogni applicazione ha almeno un AppModule radice, e le funzionalità
vengono tipicamente isolate in feature module (es. PostsModule,
AuthModule, UsersModule).
Provider
Qualsiasi classe gestita dal contenitore di Dependency Injection di Nest: service,
repository, factory, helper. Viene dichiarato in providers nel modulo e può
essere iniettato ovunque sia disponibile (nello stesso modulo, o esportato verso altri
moduli).
Middleware
Funzioni eseguite prima del routing di Nest, con accesso diretto a
req, res e next() — lo stesso modello di Express. Utile
per logging grezzo, parsing di cookie, o header custom applicati a livello globale.
Guard
Decidono se una richiesta può procedere, restituendo true/
false (o lanciando un'eccezione). Sono il posto corretto per autenticazione e
autorizzazione — mai dentro il controller o il service.
Interceptor
Si posizionano attorno all'esecuzione del gestore della rotta (come un middleware AOP): possono trasformare la richiesta prima che arrivi al controller e la risposta prima che esca. Casi d'uso tipici: logging dei tempi di risposta, trasformazione uniforme delle risposte, caching, gestione di timeout.
Pipe
Trasformano e validano i dati in ingresso (parametri di rotta, query string, body) prima che
raggiungano il controller. ValidationPipe, integrato con class-validator,
è il pipe più usato in assoluto in ogni API NestJS seria.
Exception Filter
Intercettano le eccezioni lanciate in qualsiasi punto della richiesta e le trasformano in una risposta HTTP consistente (status code, corpo JSON strutturato), evitando che stack trace o errori grezzi raggiungano il client.
Installazione
# 1. Crea un nuovo progetto NestJS
nest new blog-api
# Durante la creazione, scegli npm come package manager quando richiesto
cd blog-api
# 2. Installa le dipendenze per il database (TypeORM + driver PostgreSQL)
npm install @nestjs/typeorm typeorm pg
# 3. Installa le dipendenze per la validazione
npm install class-validator class-transformer
# 4. Installa le dipendenze per l'autenticazione JWT
npm install @nestjs/jwt @nestjs/passport passport passport-jwt bcrypt
npm install --save-dev @types/passport-jwt @types/bcrypt
# 5. Installa le dipendenze per sicurezza e rate limiting
npm install helmet @nestjs/throttler
# 6. Installa le dipendenze per l'upload di file
npm install @nestjs/platform-express multer
npm install --save-dev @types/multer
# 7. Installa Swagger per la documentazione automatica dell'API
npm install @nestjs/swagger
# 8. Installa il logger strutturato
npm install nestjs-pino pino-http pino-pretty
# 9. Configura le variabili d'ambiente
npm install @nestjs/config
Ogni comando installa un blocco funzionale ben preciso: @nestjs/typeorm +
typeorm + pg collegano Nest a PostgreSQL tramite l'ORM TypeORM;
class-validator/class-transformer abilitano i DTO validati
automaticamente; lo stack passport/passport-jwt/bcrypt
costruisce l'intero flusso di autenticazione; helmet e
@nestjs/throttler irrobustiscono la sicurezza HTTP e il rate limiting;
multer gestisce il multipart/form-data per gli upload; nestjs-pino
fornisce logging strutturato JSON, adatto alla produzione.
Implementazione Passo-Passo
1. Struttura delle cartelle
src/
├── main.ts
├── app.module.ts
├── config/
│ └── configuration.ts
├── common/
│ ├── filters/
│ │ └── http-exception.filter.ts
│ ├── interceptors/
│ │ ├── logging.interceptor.ts
│ │ └── transform.interceptor.ts
│ ├── guards/
│ │ └── roles.guard.ts
│ ├── decorators/
│ │ └── roles.decorator.ts
│ └── pipes/
│ └── parse-object-id.pipe.ts
├── auth/
│ ├── auth.module.ts
│ ├── auth.controller.ts
│ ├── auth.service.ts
│ ├── strategies/
│ │ ├── jwt.strategy.ts
│ │ └── jwt-refresh.strategy.ts
│ └── dto/
│ ├── login.dto.ts
│ └── register.dto.ts
├── users/
│ ├── users.module.ts
│ ├── users.service.ts
│ └── entities/
│ └── user.entity.ts
└── posts/
├── posts.module.ts
├── posts.controller.ts
├── posts.service.ts
├── entities/
│ └── post.entity.ts
└── dto/
├── create-post.dto.ts
└── update-post.dto.ts
2. Configurazione centralizzata con @nestjs/config
// src/config/configuration.ts
export default () => ({
port: parseInt(process.env.PORT ?? '3000', 10),
database: {
host: process.env.DB_HOST ?? 'localhost',
port: parseInt(process.env.DB_PORT ?? '5432', 10),
username: process.env.DB_USERNAME ?? 'postgres',
password: process.env.DB_PASSWORD ?? 'postgres',
name: process.env.DB_NAME ?? 'blog_db',
},
jwt: {
accessSecret: process.env.JWT_ACCESS_SECRET ?? 'change-me-access',
accessExpiresIn: process.env.JWT_ACCESS_EXPIRES_IN ?? '15m',
refreshSecret: process.env.JWT_REFRESH_SECRET ?? 'change-me-refresh',
refreshExpiresIn: process.env.JWT_REFRESH_EXPIRES_IN ?? '7d',
},
});
// src/app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ThrottlerModule } from '@nestjs/throttler';
import configuration from './config/configuration';
import { AuthModule } from './auth/auth.module';
import { UsersModule } from './users/users.module';
import { PostsModule } from './posts/posts.module';
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true, load: [configuration] }),
TypeOrmModule.forRootAsync({
inject: [ConfigService],
useFactory: (cfg: ConfigService) => ({
type: 'postgres',
host: cfg.get('database.host'),
port: cfg.get('database.port'),
username: cfg.get('database.username'),
password: cfg.get('database.password'),
database: cfg.get('database.name'),
autoLoadEntities: true,
synchronize: process.env.NODE_ENV !== 'production', // ⚠️ solo in sviluppo
}),
}),
ThrottlerModule.forRoot([{ ttl: 60000, limit: 100 }]),
AuthModule,
UsersModule,
PostsModule,
],
})
export class AppModule {}
Riga per riga: ConfigModule.forRoot({ isGlobal: true }) rende
ConfigService disponibile in tutta l'applicazione senza dover reimportare il
modulo ovunque. TypeOrmModule.forRootAsync costruisce la connessione al database
in modo asincrono, leggendo i valori dal ConfigService invece che da un oggetto
statico — necessario per poter usare variabili d'ambiente. synchronize: true
fa sì che TypeORM crei/aggiorni automaticamente le tabelle in base alle entity: comodissimo in
sviluppo, pericoloso in produzione (può cancellare dati), dove si useranno
invece le migration.
3. Entity con TypeORM
// src/users/entities/user.entity.ts
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, OneToMany } from 'typeorm';
import { Post } from '../../posts/entities/post.entity';
import { Exclude } from 'class-transformer';
export enum UserRole {
ADMIN = 'admin',
AUTHOR = 'author',
READER = 'reader',
}
@Entity('users')
export class User {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ unique: true })
email: string;
@Column()
@Exclude() // esclude l'hash della password dalle risposte serializzate
passwordHash: string;
@Column({ type: 'enum', enum: UserRole, default: UserRole.READER })
role: UserRole;
@OneToMany(() => Post, (post) => post.author)
posts: Post[];
@CreateDateColumn()
createdAt: Date;
}
// src/posts/entities/post.entity.ts
import {
Entity, Column, PrimaryGeneratedColumn, ManyToOne,
CreateDateColumn, UpdateDateColumn, Index,
} from 'typeorm';
import { User } from '../../users/entities/user.entity';
@Entity('posts')
export class Post {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
title: string;
@Index({ unique: true })
@Column()
slug: string;
@Column('text')
content: string;
@Column({ default: false })
published: boolean;
@Column({ nullable: true })
coverImageUrl?: string;
@ManyToOne(() => User, (user) => user.posts, { eager: true })
author: User;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}
4. DTO con class-validator
// src/posts/dto/create-post.dto.ts
import { IsString, IsBoolean, IsOptional, MinLength, MaxLength } from 'class-validator';
export class CreatePostDto {
@IsString()
@MinLength(5)
@MaxLength(200)
title: string;
@IsString()
@MinLength(20)
content: string;
@IsBoolean()
@IsOptional()
published?: boolean;
}
// src/posts/dto/update-post.dto.ts
import { PartialType } from '@nestjs/mapped-types';
import { CreatePostDto } from './create-post.dto';
export class UpdatePostDto extends PartialType(CreatePostDto) {}
PartialType genera automaticamente una versione dove tutti i campi del DTO
originale diventano opzionali — perfetto per gli update parziali (PATCH), senza
duplicare i decoratori di validazione.
5. Abilitare globalmente ValidationPipe
// src/main.ts
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import helmet from 'helmet';
import { AppModule } from './app.module';
import { HttpExceptionFilter } from './common/filters/http-exception.filter';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.use(helmet());
app.enableCors({ origin: process.env.CORS_ORIGIN?.split(',') ?? '*' });
app.useGlobalPipes(
new ValidationPipe({
whitelist: true, // rimuove le proprietà non presenti nel DTO
forbidNonWhitelisted: true, // lancia errore se arrivano proprietà extra
transform: true, // converte automaticamente i tipi (es. string → number)
}),
);
app.useGlobalFilters(new HttpExceptionFilter());
app.setGlobalPrefix('api/v1');
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();
whitelist: true + forbidNonWhitelisted: true sono la coppia più
importante per la sicurezza dei DTO: senza di essa, un client potrebbe inviare campi extra
(es. role: 'admin' in una richiesta di registrazione) che TypeORM potrebbe
inavvertitamente persistere se il codice non li filtra esplicitamente altrove.
6. Middleware custom
// src/common/middleware/request-id.middleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { randomUUID } from 'crypto';
@Injectable()
export class RequestIdMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction): void {
req['requestId'] = randomUUID();
res.setHeader('X-Request-Id', req['requestId']);
next();
}
}
// registrazione in app.module.ts
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(RequestIdMiddleware).forRoutes('*');
}
}
7. Guard: JwtAuthGuard e RolesGuard
// src/auth/strategies/jwt.strategy.ts
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(config: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: config.get('jwt.accessSecret'),
});
}
async validate(payload: { sub: string; email: string; role: string }) {
// Il valore restituito viene allegato a req.user
return { userId: payload.sub, email: payload.email, role: payload.role };
}
}
// src/common/decorators/roles.decorator.ts
import { SetMetadata } from '@nestjs/common';
import { UserRole } from '../../users/entities/user.entity';
export const ROLES_KEY = 'roles';
export const Roles = (...roles: UserRole[]) => SetMetadata(ROLES_KEY, roles);
// src/common/guards/roles.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ROLES_KEY } from '../decorators/roles.decorator';
import { UserRole } from '../../users/entities/user.entity';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride(ROLES_KEY, [
context.getHandler(),
context.getClass(),
]);
if (!requiredRoles) return true; // nessun vincolo di ruolo sulla rotta
const { user } = context.switchToHttp().getRequest();
return requiredRoles.includes(user?.role);
}
}
Il pattern Reflector.getAllAndOverride legge i metadati impostati dal decoratore
@Roles(...) sia a livello di singolo metodo che di intera classe, permettendo di
definire un vincolo di ruolo su un intero controller e sovrascriverlo su singole rotte quando
necessario.
8. Interceptor: logging e trasformazione della risposta
// src/common/interceptors/logging.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler, Logger } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
private readonly logger = new Logger('HTTP');
intercept(context: ExecutionContext, next: CallHandler): Observable {
const req = context.switchToHttp().getRequest();
const start = Date.now();
return next.handle().pipe(
tap(() => {
const ms = Date.now() - start;
this.logger.log(`${req.method} ${req.url} — ${ms}ms`);
}),
);
}
}
// src/common/interceptors/transform.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
export interface Response {
success: true;
data: T;
timestamp: string;
}
@Injectable()
export class TransformInterceptor implements NestInterceptor> {
intercept(context: ExecutionContext, next: CallHandler): Observable> {
return next.handle().pipe(
map((data) => ({
success: true,
data,
timestamp: new Date().toISOString(),
})),
);
}
}
9. Exception Filter globale
// src/common/filters/http-exception.filter.ts
import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus, Logger } from '@nestjs/common';
import { Request, Response } from 'express';
@Catch()
export class HttpExceptionFilter implements ExceptionFilter {
private readonly logger = new Logger('ExceptionFilter');
catch(exception: unknown, host: ArgumentsHost): void {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const request = ctx.getRequest();
const status = exception instanceof HttpException
? exception.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR;
const message = exception instanceof HttpException
? exception.getResponse()
: 'Errore interno del server';
// Logga lo stack trace completo solo lato server, mai nella risposta al client
if (!(exception instanceof HttpException)) {
this.logger.error(exception instanceof Error ? exception.stack : exception);
}
response.status(status).json({
success: false,
statusCode: status,
path: request.url,
timestamp: new Date().toISOString(),
message,
});
}
}
Un dettaglio di sicurezza cruciale: quando l'eccezione non è una
HttpException conosciuta (quindi un errore inatteso, es. un bug o un errore del
database), il messaggio restituito al client è generico ("Errore interno del server") — lo
stack trace reale viene loggato solo lato server. Esporre stack trace ai client è un problema
di sicurezza noto (information disclosure).
10. Pipe custom
// src/common/pipes/parse-uuid-or-404.pipe.ts
import { PipeTransform, Injectable, ArgumentMetadata, NotFoundException } from '@nestjs/common';
import { isUUID } from 'class-validator';
@Injectable()
export class ParseUuidOr404Pipe implements PipeTransform {
transform(value: string, _metadata: ArgumentMetadata): string {
if (!isUUID(value)) {
throw new NotFoundException(`Risorsa con id "${value}" non trovata`);
}
return value;
}
}
Restituire un 404 invece di un generico 400 Bad Request quando l'id
non è nemmeno un UUID valido è una scelta deliberata: dal punto di vista del client, "risorsa
non trovata" è semanticamente più corretto e non rivela dettagli sul formato interno degli id.
Esempio Reale: API Blog Completa
Mettiamo insieme tutti i pezzi in un modulo PostsModule completo, con CRUD,
autenticazione, autorizzazione basata sui ruoli e upload dell'immagine di copertina.
Auth Service: login, register, refresh token
// src/auth/auth.service.ts
import { Injectable, UnauthorizedException, ConflictException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
import * as bcrypt from 'bcrypt';
import { UsersService } from '../users/users.service';
import { RegisterDto } from './dto/register.dto';
import { LoginDto } from './dto/login.dto';
@Injectable()
export class AuthService {
constructor(
private usersService: UsersService,
private jwtService: JwtService,
private config: ConfigService,
) {}
async register(dto: RegisterDto) {
const existing = await this.usersService.findByEmail(dto.email);
if (existing) throw new ConflictException('Email già registrata');
const passwordHash = await bcrypt.hash(dto.password, 12);
const user = await this.usersService.create({ ...dto, passwordHash });
return this.buildTokens(user.id, user.email, user.role);
}
async login(dto: LoginDto) {
const user = await this.usersService.findByEmail(dto.email);
if (!user) throw new UnauthorizedException('Credenziali non valide');
const passwordValid = await bcrypt.compare(dto.password, user.passwordHash);
if (!passwordValid) throw new UnauthorizedException('Credenziali non valide');
return this.buildTokens(user.id, user.email, user.role);
}
async refresh(userId: string, email: string, role: string) {
// In produzione: verifica anche che il refresh token sia ancora valido lato DB
// (whitelist/blacklist) per poterlo revocare, es. al logout.
return this.buildTokens(userId, email, role);
}
private buildTokens(sub: string, email: string, role: string) {
const payload = { sub, email, role };
const accessToken = this.jwtService.sign(payload, {
secret: this.config.get('jwt.accessSecret'),
expiresIn: this.config.get('jwt.accessExpiresIn'),
});
const refreshToken = this.jwtService.sign(payload, {
secret: this.config.get('jwt.refreshSecret'),
expiresIn: this.config.get('jwt.refreshExpiresIn'),
});
return { accessToken, refreshToken };
}
}
Perché due token separati? L'access token ha una vita breve (15 minuti) ed è quello inviato ad ogni richiesta — se venisse rubato, il danno è limitato nel tempo. Il refresh token ha una vita più lunga (giorni) ma viene usato solo per ottenere un nuovo access token su un endpoint dedicato, riducendo la superficie di attacco.
Auth Controller
// src/auth/auth.controller.ts
import { Controller, Post, Body, UseGuards, Req } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { AuthService } from './auth.service';
import { RegisterDto } from './dto/register.dto';
import { LoginDto } from './dto/login.dto';
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
@Post('register')
register(@Body() dto: RegisterDto) {
return this.authService.register(dto);
}
@Post('login')
login(@Body() dto: LoginDto) {
return this.authService.login(dto);
}
@UseGuards(AuthGuard('jwt-refresh'))
@Post('refresh')
refresh(@Req() req) {
return this.authService.refresh(req.user.userId, req.user.email, req.user.role);
}
}
Posts Controller: CRUD, RBAC e upload
// src/posts/posts.controller.ts
import {
Controller, Get, Post, Body, Patch, Param, Delete,
UseGuards, UseInterceptors, UploadedFile, Query,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { AuthGuard } from '@nestjs/passport';
import { PostsService } from './posts.service';
import { CreatePostDto } from './dto/create-post.dto';
import { UpdatePostDto } from './dto/update-post.dto';
import { Roles } from '../common/decorators/roles.decorator';
import { RolesGuard } from '../common/guards/roles.guard';
import { UserRole } from '../users/entities/user.entity';
import { ParseUuidOr404Pipe } from '../common/pipes/parse-uuid-or-404.pipe';
import { CurrentUser } from '../common/decorators/current-user.decorator';
@Controller('posts')
export class PostsController {
constructor(private postsService: PostsService) {}
@Get()
findAll(@Query('page') page = 1, @Query('limit') limit = 10) {
return this.postsService.findAll(Number(page), Number(limit));
}
@Get(':id')
findOne(@Param('id', ParseUuidOr404Pipe) id: string) {
return this.postsService.findOne(id);
}
@UseGuards(AuthGuard('jwt'), RolesGuard)
@Roles(UserRole.AUTHOR, UserRole.ADMIN)
@Post()
create(@Body() dto: CreatePostDto, @CurrentUser() user) {
return this.postsService.create(dto, user.userId);
}
@UseGuards(AuthGuard('jwt'), RolesGuard)
@Roles(UserRole.AUTHOR, UserRole.ADMIN)
@Patch(':id')
update(@Param('id', ParseUuidOr404Pipe) id: string, @Body() dto: UpdatePostDto, @CurrentUser() user) {
return this.postsService.update(id, dto, user.userId, user.role);
}
@UseGuards(AuthGuard('jwt'), RolesGuard)
@Roles(UserRole.ADMIN)
@Delete(':id')
remove(@Param('id', ParseUuidOr404Pipe) id: string) {
return this.postsService.remove(id);
}
@UseGuards(AuthGuard('jwt'))
@Post(':id/cover')
@UseInterceptors(FileInterceptor('file', {
limits: { fileSize: 5 * 1024 * 1024 }, // 5MB
fileFilter: (_req, file, cb) => {
const allowed = ['image/jpeg', 'image/png', 'image/webp'];
cb(null, allowed.includes(file.mimetype));
},
}))
uploadCover(@Param('id', ParseUuidOr404Pipe) id: string, @UploadedFile() file: Express.Multer.File) {
return this.postsService.setCoverImage(id, file);
}
}
Nota come ogni rotta applichi solo i guard e i ruoli strettamente necessari:
findAll/findOne sono pubbliche (nessun guard), create/
update richiedono ruolo author o admin, mentre
remove è riservata al solo admin — un esempio concreto di
principio del privilegio minimo applicato rotta per rotta.
Posts Service
// src/posts/posts.service.ts
import { Injectable, NotFoundException, ForbiddenException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Post } from './entities/post.entity';
import { CreatePostDto } from './dto/create-post.dto';
import { UpdatePostDto } from './dto/update-post.dto';
import { UserRole } from '../users/entities/user.entity';
import slugify from 'slugify';
@Injectable()
export class PostsService {
constructor(@InjectRepository(Post) private postsRepo: Repository) {}
async findAll(page: number, limit: number) {
const [items, total] = await this.postsRepo.findAndCount({
where: { published: true },
order: { createdAt: 'DESC' },
skip: (page - 1) * limit,
take: limit,
});
return { items, total, page, limit };
}
async findOne(id: string): Promise {
const post = await this.postsRepo.findOne({ where: { id } });
if (!post) throw new NotFoundException(`Post ${id} non trovato`);
return post;
}
async create(dto: CreatePostDto, authorId: string): Promise {
const post = this.postsRepo.create({
...dto,
slug: slugify(dto.title, { lower: true, strict: true }),
author: { id: authorId } as any,
});
return this.postsRepo.save(post);
}
async update(id: string, dto: UpdatePostDto, userId: string, userRole: UserRole): Promise {
const post = await this.findOne(id);
// Un author può modificare solo i propri post; l'admin può modificarli tutti
if (userRole !== UserRole.ADMIN && post.author.id !== userId) {
throw new ForbiddenException('Non puoi modificare un post di un altro autore');
}
Object.assign(post, dto);
return this.postsRepo.save(post);
}
async remove(id: string): Promise {
const result = await this.postsRepo.delete(id);
if (result.affected === 0) throw new NotFoundException(`Post ${id} non trovato`);
}
async setCoverImage(id: string, file: Express.Multer.File): Promise {
const post = await this.findOne(id);
// In produzione: carica su S3/Cloud Storage invece che su disco locale
post.coverImageUrl = `/uploads/${file.filename}`;
return this.postsRepo.save(post);
}
}
Osserva il controllo userRole !== UserRole.ADMIN && post.author.id !== userId
nel metodo update: è un esempio di autorizzazione a livello di risorsa
(non solo di rotta) — un author autenticato con un token JWT valido può comunque essere
bloccato se sta tentando di modificare la risorsa di qualcun altro. Questo controllo va sempre
fatto nel service, mai delegato solo al guard, che opera a un livello troppo generico per
conoscere il proprietario della singola risorsa.
Best Practice
| Principio | Applicazione pratica in NestJS |
|---|---|
| Clean Architecture | Separazione netta tra controller (HTTP), service (business logic) e repository (persistenza). Il dominio non deve mai dipendere da dettagli infrastrutturali come Express o TypeORM. |
| SOLID — Single Responsibility | Un controller mappa richieste HTTP, un service contiene una singola area di business logic, un repository un solo aggregate. |
| SOLID — Dependency Inversion | I service dipendono da interfacce/token astratti (es. @InjectRepository), non da implementazioni concrete — sostituibili nei test con mock. |
| DRY | Logica di validazione condivisa nei DTO con PartialType/PickType/OmitType, evitando duplicazione tra Create e Update DTO. |
| KISS | Evita di introdurre CQRS, Event Sourcing o microservizi finché la complessità reale del dominio non lo giustifica. |
| Repository Pattern | TypeORM/Prisma già forniscono questo layer; evita di "bucare" l'astrazione chiamando query SQL grezze direttamente nei service salvo casi di performance critica. |
| DTO | Ogni endpoint ha un DTO di input dedicato — mai esporre direttamente le entity del database come contratto di API. |
| Validation | ValidationPipe globale con whitelist e forbidNonWhitelisted sempre attivi in ogni progetto, senza eccezioni. |
Performance
Caching
// Caching a livello di endpoint con @nestjs/cache-manager
import { CacheInterceptor, CacheTTL } from '@nestjs/cache-manager';
import { UseInterceptors } from '@nestjs/common';
@UseInterceptors(CacheInterceptor)
@CacheTTL(60) // cache per 60 secondi
@Get()
findAll() {
return this.postsService.findAll(1, 10);
}
Lazy Loading dei moduli
In applicazioni monolitiche di grandi dimensioni, LazyModuleLoader permette di
caricare moduli poco usati (es. un modulo di reportistica) solo quando effettivamente
richiesti, riducendo il tempo di bootstrap.
Query ottimizzate
- Usa
selectesplicito in TypeORM per evitare di trasferire colonne non necessarie (es. escluderecontentnelle liste). - Aggiungi indici sulle colonne usate in
WHERE/ORDER BYfrequenti (vedi@Indexsullo slug nell'entityPost). - Evita il problema N+1: usa
relationsoQueryBuilderconleftJoinAndSelectinvece di caricare le relazioni in loop. - Usa sempre paginazione (
skip/take) — non restituire mai collezioni illimitate.
Logging e Monitoraggio
In produzione, sostituisci il logger di default con nestjs-pino per log
strutturati JSON, facilmente indicizzabili da strumenti come Grafana Loki o Datadog. Per il
monitoraggio delle performance, integra @nestjs/terminus per gli health check
(/health) usati da load balancer e orchestratori (Kubernetes, Railway).
Profiling
Per individuare colli di bottiglia reali, usa node --prof o strumenti APM
(Application Performance Monitoring) come New Relic o Elastic APM, che tracciano il tempo
speso in ogni handler, query e chiamata esterna — evita di ottimizzare "a sensazione" senza
dati reali.
Sicurezza
| Misura | Come si implementa |
|---|---|
| JWT | Access token a vita breve firmato con secret robusto (32+ caratteri random), mai hardcoded nel codice. |
| Refresh Token | Vita più lunga, idealmente con possibilità di revoca (whitelist in Redis/DB), rotazione ad ogni uso. |
| Hash Password / bcrypt | bcrypt.hash(password, 12) — mai MD5/SHA1, mai password in chiaro nel DB. |
| Helmet | app.use(helmet()) imposta header di sicurezza (CSP, X-Frame-Options, HSTS) con un'unica riga. |
| CORS | app.enableCors({ origin: [...] }) con whitelist esplicita di domini, mai origin: '*' in produzione se l'API richiede credenziali. |
| Rate Limiting | @nestjs/throttler per limitare le richieste per IP, essenziale su endpoint come /auth/login per mitigare brute-force. |
| ValidationPipe | whitelist + forbidNonWhitelisted per bloccare mass assignment su campi non previsti. |
| Sanitizzazione Input | class-validator per la struttura, più sanitizzazione esplicita dell'HTML (es. sanitize-html) se il campo permette markup libero, per prevenire XSS. |
// Esempio: rate limiting più aggressivo specifico sul login
import { Throttle } from '@nestjs/throttler';
@Throttle({ default: { limit: 5, ttl: 60000 } }) // max 5 tentativi al minuto per IP
@Post('login')
login(@Body() dto: LoginDto) {
return this.authService.login(dto);
}
Errori Comuni
1. Dimenticare whitelist/forbidNonWhitelisted nel ValidationPipe
Problema: un client può inviare campi extra non previsti dal DTO (es. role: 'admin').
Causa: ValidationPipe configurato senza whitelist: true.
Soluzione: abilita sempre whitelist e forbidNonWhitelisted globalmente in main.ts.
2. synchronize: true in produzione
Problema: TypeORM può alterare/cancellare colonne o tabelle a runtime.
Causa: confusione tra ambiente di sviluppo e produzione nella configurazione database.
Soluzione: synchronize: false in produzione, gestisci lo schema con migration esplicite (typeorm migration:generate).
3. Circular dependency tra moduli
Problema: errore "Nest cannot resolve dependencies" o crash silenzioso all'avvio.
Causa: due moduli si importano a vicenda direttamente.
Soluzione: usa forwardRef(() => ModuleB) in entrambi i moduli, oppure — meglio — estrai la dipendenza condivisa in un terzo modulo.
4. Esporre le entity direttamente come risposta API
Problema: campi sensibili (es. passwordHash) finiscono nella risposta JSON.
Causa: il controller restituisce l'entity TypeORM così com'è, senza un layer DTO/serializer.
Soluzione: usa @Exclude() di class-transformer sull'entity insieme a ClassSerializerInterceptor globale, o meglio ancora un DTO di risposta esplicito.
5. Password salvate senza hashing
Problema: compromissione del DB espone tutte le password in chiaro.
Causa: omissione (spesso durante il rapid prototyping) del passaggio di hashing.
Soluzione: hash con bcrypt/argon2 sempre, anche in sviluppo — non c'è motivo valido per saltarlo mai.
6. Secret JWT deboli o hardcoded nel codice
Problema: un secret prevedibile permette la falsificazione di token validi.
Causa: secret di default lasciato in produzione, o committato nel repository.
Soluzione: secret generati casualmente (almeno 256 bit), gestiti solo tramite variabili d'ambiente/secret manager, mai nel codice sorgente.
7. Mancata gestione degli errori asincroni nei service
Problema: promise rejection non gestite causano crash del processo Node.
Causa: chiamate async senza try/catch dove serve, o dimenticanza dell'exception filter globale.
Soluzione: lascia che le eccezioni salgano fino all'exception filter globale (comportamento di default di Nest), intercetta solo dove serve un comportamento diverso (fallback, retry).
8. N+1 query non individuate
Problema: l'endpoint diventa lentissimo con la crescita dei dati.
Causa: caricamento di relazioni dentro un loop invece che con un singolo join.
Soluzione: usa relations/leftJoinAndSelect, monitora le query generate con logging: true in sviluppo.
9. CORS aperto a tutti in produzione con credentials
Problema: qualsiasi sito web può effettuare richieste autenticate all'API a nome dell'utente.
Causa: origin: '*' combinato con credentials: true.
Soluzione: whitelist esplicita dei domini autorizzati.
10. Nessun rate limiting sugli endpoint di autenticazione
Problema: attacchi brute-force su login/registrazione.
Causa: @nestjs/throttler non applicato o con soglie troppo permissive.
Soluzione: @Throttle più restrittivo specificamente su login/register/reset-password.
11. DTO di Update identico al DTO di Create senza PartialType
Problema: ogni PATCH richiede tutti i campi obbligatori, rompendo gli update parziali.
Causa: copia/incolla del DTO Create senza rendere i campi opzionali.
Soluzione: class UpdateXDto extends PartialType(CreateXDto) {}.
12. Guard applicati solo a livello di rotta, mai di risorsa
Problema: un utente autenticato può modificare risorse di altri utenti.
Causa: ci si affida solo a @UseGuards(AuthGuard('jwt')) senza controllo di ownership nel service.
Soluzione: verifica esplicita resource.ownerId === user.id (o ruolo admin) nel service, come mostrato nell'esempio PostsService.update.
13. Connessione al database senza pool configurato correttamente
Problema: esaurimento delle connessioni sotto carico, errori "too many clients".
Causa: valori di default del pool non adeguati al traffico reale.
Soluzione: configura esplicitamente extra: { max: 20 } nella connection di TypeORM, calibrato sul piano del database.
14. Upload di file senza limiti di dimensione o tipo
Problema: un utente malintenzionato carica file enormi o eseguibili mascherati da immagini.
Causa: FileInterceptor configurato senza limits né fileFilter.
Soluzione: imposta sempre limits.fileSize e valida il mimetype, come nell'esempio uploadCover.
15. Variabili d'ambiente non validate all'avvio
Problema: l'app si avvia "silenziosamente" con configurazione incompleta e fallisce in modo criptico più tardi.
Causa: nessuno schema di validazione sulle env var.
Soluzione: usa ConfigModule.forRoot({ validationSchema: Joi.object({...}) }) per far fallire il bootstrap immediatamente se manca una variabile critica.
16. Logging di dati sensibili
Problema: password, token o dati personali finiscono nei log in chiaro.
Causa: logging indiscriminato dell'intero body della richiesta.
Soluzione: redigi (mascherare) esplicitamente i campi sensibili prima del logging, o usa i redact path di pino.
17. Test scritti solo per il "happy path"
Problema: bug che emergono solo su input non validi o edge case restano invisibili fino alla produzione.
Causa: pressione sui tempi, copertura di test solo sul flusso principale.
Soluzione: testa esplicitamente errori 4xx attesi (validazione, permessi, not found), non solo i 2xx.
18. Versionamento dell'API assente
Problema: ogni modifica breaking rompe i client esistenti senza preavviso.
Causa: nessun prefisso di versione (/api/v1) o strategia di versioning Nest configurata.
Soluzione: app.setGlobalPrefix('api/v1') fin dal primo giorno, anche su progetti piccoli.
19. Dipendenze non aggiornate con vulnerabilità note
Problema: l'API resta esposta a CVE pubblicamente documentate.
Causa: mancanza di un processo di audit periodico delle dipendenze.
Soluzione: npm audit integrato in CI, aggiornamenti regolari con Dependabot/Renovate.
20. Nessuna distinzione tra errori di validazione e errori di dominio
Problema: tutti gli errori tornano come generico 500, rendendo impossibile per il client distinguere un input errato da un problema del server.
Causa: uso di Error generico invece delle classi HttpException specifiche di Nest.
Soluzione: usa sempre le eccezioni tipizzate corrette (BadRequestException, NotFoundException, ForbiddenException, ConflictException...) — è il modo in cui Nest comunica lo status code HTTP corretto.
FAQ
1. NestJS è adatto anche a progetti piccoli?
Sì, ma il vero valore emerge quando il progetto cresce: su un piccolo script, la struttura può sembrare eccessiva rispetto a Express puro.
2. Devo usare per forza TypeScript?
Tecnicamente NestJS supporta anche JavaScript puro, ma si perde gran parte del valore (decoratori tipizzati, DI type-safe). È fortemente sconsigliato in produzione.
3. TypeORM o Prisma?
TypeORM si integra più nativamente con i decoratori NestJS ed è più maturo nell'ecosistema Nest; Prisma offre un client generato più type-safe e una migliore developer experience nelle migration, ma richiede un layer di integrazione manuale con Nest.
4. Come gestisco le migration del database?
Con TypeORM: typeorm migration:generate per generarle dal diff delle entity, typeorm migration:run per applicarle in CI/CD, mai synchronize: true in produzione.
5. Come strutturo un progetto molto grande?
Modularizza per dominio (feature module), non per tipo tecnico — evita cartelle globali "controllers/", "services/" che mescolano domini diversi.
6. Come faccio testing dei controller?
Con Test.createTestingModule di @nestjs/testing, mockando i service iniettati; per i service, mocka i repository TypeORM con getRepositoryToken.
7. Qual è la differenza tra Guard e Middleware per l'autenticazione?
Il middleware non ha accesso al contesto di esecuzione di Nest (es. metadati dei decoratori), quindi non può leggere @Roles(). I guard sì — per questo l'autenticazione/autorizzazione va sempre nei guard, non nei middleware.
8. Posso usare GraphQL invece di REST?
Sì, NestJS ha supporto ufficiale sia per Apollo Server che per Mercurius, con lo stesso sistema di moduli/DI — utile se hai bisogno di query flessibili lato client.
9. Come gestisco le transazioni database?
Con DataSource.transaction() di TypeORM, o con il decoratore @Transactional() della libreria typeorm-transactional per una sintassi più dichiarativa.
10. Come implemento il refresh token in modo sicuro?
Memorizza un hash del refresh token lato DB (whitelist), ruotalo ad ogni utilizzo (token rotation), e invalidalo esplicitamente al logout.
11. NestJS supporta i microservizi?
Sì, nativamente, con transport layer per TCP, Redis, RabbitMQ, Kafka, gRPC e NATS tramite @nestjs/microservices.
12. Come faccio il deploy in produzione?
Build con nest build (compila in dist/), poi node dist/main.js; su piattaforme come Railway/Render/Fly.io basta un Dockerfile multi-stage o il buildpack Node nativo.
13. Come gestisco variabili d'ambiente diverse per dev/staging/prod?
ConfigModule.forRoot({ envFilePath: ['.env.${NODE_ENV}', '.env'] }), con le variabili reali iniettate dalla piattaforma di hosting in produzione, non da file .env committati.
14. Come implemento upload verso S3 invece che su disco locale?
Sostituisci lo storage engine di Multer con multer-s3, oppure gestisci l'upload manualmente nel service con l'SDK AWS dopo aver ricevuto il buffer in memoria.
15. Come documento automaticamente l'API?
Con @nestjs/swagger: decoratori @ApiTags, @ApiProperty sui DTO, e SwaggerModule.setup() in main.ts generano una UI OpenAPI interattiva su /api/docs.
16. Come implemento soft delete?
TypeORM supporta nativamente @DeleteDateColumn() sull'entity: softDelete() imposta la colonna invece di eliminare la riga, e le query escludono automaticamente i record eliminati.
17. Come gestisco versioni multiple dell'API contemporaneamente?
Con app.enableVersioning({ type: VersioningType.URI }) e il decoratore @Version('2') sui singoli controller/metodi che devono coesistere con la v1.
18. È necessario un ORM o posso usare query SQL dirette?
Per la maggior parte dei casi un ORM riduce errori e boilerplate; per query molto complesse o performance-critical, TypeORM permette comunque query raw tramite QueryRunner mantenendo il resto dell'app sull'ORM.
19. Come implemento WebSocket in NestJS?
Con @nestjs/websockets e un @WebSocketGateway(), che si integra con Socket.IO o ws mantenendo lo stesso sistema di DI e guard delle rotte REST.
20. Come gestisco i cron job?
Con @nestjs/schedule e il decoratore @Cron('0 0 * * *') su un metodo di un service — utile per pulizia dati, invio digest email, sincronizzazioni periodiche.
21. Come proteggo l'API da attacchi di tipo SQL Injection?
TypeORM/Prisma parametrizzano automaticamente le query — il rischio emerge solo se costruisci query raw concatenando stringhe manualmente, cosa da evitare sempre.
22. Qual è il modo corretto di gestire i secrets in produzione?
Mai nel codice o in file committati: usa le variabili d'ambiente della piattaforma di hosting o un secret manager dedicato (AWS Secrets Manager, Doppler, Railway Variables).
23. Come scalo orizzontalmente un'API NestJS?
L'app deve essere stateless (nessuna sessione in memoria locale — usa JWT o Redis per lo stato condiviso), poi si replica dietro un load balancer; per WebSocket serve un adapter Redis per sincronizzare le connessioni tra istanze.
24. Come gestisco la retrocompatibilità quando cambio uno schema di risposta?
Introduci un nuovo campo invece di rinominare quello esistente, deprecalo gradualmente con documentazione chiara, e usa il versioning dell'API per i cambiamenti realmente breaking.
25. Vale la pena scrivere test E2E oltre agli unit test per un'API NestJS?
Sì — @nestjs/testing con supertest permette di testare l'intera pipeline (guard, pipe, interceptor inclusi) chiamando gli endpoint reali su un'istanza dell'app in memoria, catturando problemi di integrazione che gli unit test da soli non vedono.
Risorse e Documentazione Ufficiale
Per approfondire ogni singolo argomento trattato in questa guida, la documentazione ufficiale resta sempre la fonte più affidabile e aggiornata:
- NestJS — documentazione ufficiale:
docs.nestjs.com, in particolare le sezioni su Guard, Interceptor, Pipe ed Exception Filter. - TypeORM — documentazione ufficiale:
typeorm.io, per relazioni, migration e query builder avanzato. - Passport.js:
passportjs.org, la libreria di autenticazione su cui si basa@nestjs/passport. - class-validator: repository ufficiale su GitHub, con l'elenco completo dei decoratori di validazione disponibili oltre a quelli usati in questa guida.
- OWASP API Security Top 10: la checklist di riferimento per la sicurezza delle API REST, complementare alla sezione Sicurezza di questa guida.
- jwt.io: strumento per ispezionare e decodificare token JWT durante il debug dell'autenticazione.
Articoli Correlati Consigliati
Questa guida copre l'intero ciclo di vita di una REST API NestJS, ma ogni sezione può diventare un approfondimento a sé stante. Ecco i temi naturalmente collegati che vale la pena esplorare in seguito:
- Modularizzare un progetto NestJS di grandi dimensioni: feature module, shared module e barrel file.
- Dependency Injection in NestJS spiegata con esempi: provider custom, factory provider, token di iniezione.
- Autenticazione JWT e Refresh Token in profondità: token rotation, revoca e gestione multi-dispositivo.
- Role Based Access Control (RBAC) avanzato: permessi granulari oltre ai semplici ruoli.
- Validazione con ValidationPipe e class-validator: validatori custom e messaggi di errore localizzati.
- Logging centralizzato in NestJS con Pino e correlazione delle richieste tramite request ID.
- Exception Filter personalizzati per domini specifici (es. errori di pagamento, errori di dominio).
- Upload di file avanzato con Multer: storage su S3, validazione del contenuto reale dei file.
- Scheduler e Cron Job in NestJS con
@nestjs/schedule. - WebSocket con NestJS: notifiche realtime e scaling con Redis adapter.
- Testing di controller e service NestJS: unit test, mock dei repository, test E2E con Supertest.
Conclusione
Costruire una REST API con NestJS significa investire in una struttura che scala con la complessità del progetto: moduli ben isolati, Dependency Injection per la testabilità, guard/interceptor/pipe per separare i cross-cutting concern dalla logica di business, e un ecosistema — TypeORM, Passport, class-validator, Swagger — che copre in modo maturo ogni esigenza reale di produzione. L'esempio Blog API costruito in questa guida (CRUD, JWT con refresh token, RBAC, upload file, logging centralizzato, gestione degli errori) è lo scheletro riutilizzabile per la maggior parte delle API NestJS che incontrerai nella pratica.
Prossimi passi consigliati: approfondisci la modularizzazione di progetti NestJS di grandi dimensioni, l'autenticazione JWT e refresh token nel dettaglio, il pattern RBAC con permessi granulari, e le tecniche di testing per controller e service — argomenti che completano naturalmente questa guida.