# NestJS: Creare una REST API completa da zero > Guida passo dopo passo per costruire una REST API pronta per la produzione con NestJS, TypeScript, Prisma e class-validator. CRUD, validazione, gestione errori e interceptor. - Published: 2026-01-13 - Updated: 2026-04-07 - Author: SharpSkill - Tags: nestjs, nodejs, typescript, rest api, backend - Reading time: 15 min --- NestJS si e' affermato come il framework Node.js di riferimento per la costruzione di applicazioni server-side scalabili. L'architettura modulare, il sistema di dependency injection integrato e il supporto nativo per TypeScript rendono NestJS la scelta ottimale per REST API professionali. Con NestJS 11, le prestazioni e la velocita' di sviluppo raggiungono un livello ancora superiore. > **Perche' NestJS nel 2026?** > > NestJS 11 introduce uno scanning dei moduli ottimizzato, tempi di avvio piu' rapidi e decorator migliorati. Il framework combina i migliori concetti di Angular (decorator, moduli, DI) con la flessibilita' di Node.js, offrendo un'esperienza di sviluppo di primo livello per progetti backend. ## Installazione e configurazione del progetto La CLI di NestJS genera un progetto preconfigurato con TypeScript, ESLint e una struttura di cartelle ben organizzata. L'avvio richiede solo pochi comandi. ```bash # terminal # Global installation of the NestJS CLI npm install -g @nestjs/cli # Create a new project nest new my-api # Navigate to the project cd my-api # Start in development mode with hot-reload npm run start:dev ``` Il file `main.ts` rappresenta il punto di ingresso dell'applicazione. Qui vengono configurate le pipe globali, il prefisso delle rotte e altre impostazioni. ```typescript // src/main.ts import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { ValidationPipe } from '@nestjs/common'; async function bootstrap() { // Create the NestJS application const app = await NestFactory.create(AppModule); // Enable global validation app.useGlobalPipes(new ValidationPipe({ whitelist: true, // Removes non-decorated properties forbidNonWhitelisted: true, // Rejects requests with unknown properties transform: true, // Transforms payloads to DTO instances })); // Configure global prefix for all routes app.setGlobalPrefix('api'); await app.listen(3000); } bootstrap(); ``` La `ValidationPipe` con `whitelist: true` rimuove automaticamente i campi non definiti nel DTO. Con `forbidNonWhitelisted: true`, viene restituito un errore se vengono inviati campi sconosciuti. Questo protegge l'API da dati indesiderati. ## Comprendere l'architettura modulare NestJS organizza il codice in moduli. Ogni modulo incapsula controller, service ed entita' correlati. Il modulo radice importa tutti i moduli funzionali e costituisce il punto di partenza dell'applicazione. ```typescript // src/app.module.ts import { Module } from '@nestjs/common'; import { UsersModule } from './users/users.module'; import { ProductsModule } from './products/products.module'; import { AuthModule } from './auth/auth.module'; // The root module imports all application modules @Module({ imports: [ UsersModule, // User management ProductsModule, // Product catalog AuthModule, // Authentication ], }) export class AppModule {} ``` Ogni modulo funzionale segue lo stesso schema: controller per le richieste HTTP, service per la logica di business ed exports per l'utilizzo tra moduli. ```typescript // src/users/users.module.ts import { Module } from '@nestjs/common'; import { UsersController } from './users.controller'; import { UsersService } from './users.service'; @Module({ // Controllers handle HTTP requests controllers: [UsersController], // Providers are injectable throughout the module providers: [UsersService], // Exports make providers available to other modules exports: [UsersService], }) export class UsersModule {} ``` Questa struttura impone una netta separazione delle responsabilita'. Il `UsersService` viene iniettato nel controller tramite dependency injection. Grazie all'array `exports`, il service puo' essere utilizzato anche in altri moduli, come l'`AuthModule`. ## Costruire un controller CRUD completo Il controller definisce gli endpoint HTTP e delega la logica di business al service. I decorator di NestJS come `@Get()`, `@Post()`, `@Put()` e `@Delete()` associano i metodi ai verbi HTTP corrispondenti. ```typescript // src/users/users.controller.ts import { Controller, Get, Post, Put, Delete, Body, Param, Query, HttpCode, HttpStatus, ParseIntPipe, } from '@nestjs/common'; import { UsersService } from './users.service'; import { CreateUserDto } from './dto/create-user.dto'; import { UpdateUserDto } from './dto/update-user.dto'; import { User } from './entities/user.entity'; // Route prefix: /api/users @Controller('users') export class UsersController { // Inject service via constructor constructor(private readonly usersService: UsersService) {} // POST /api/users - Create a user @Post() @HttpCode(HttpStatus.CREATED) async create(@Body() createUserDto: CreateUserDto): Promise { // The DTO is automatically validated before reaching here return this.usersService.create(createUserDto); } // GET /api/users - List with pagination @Get() async findAll( @Query('page', new ParseIntPipe({ optional: true })) page: number = 1, @Query('limit', new ParseIntPipe({ optional: true })) limit: number = 10, ): Promise<{ data: User[]; total: number }> { return this.usersService.findAll(page, limit); } // GET /api/users/:id - Retrieve by ID @Get(':id') async findOne(@Param('id', ParseIntPipe) id: number): Promise { // ParseIntPipe automatically converts and validates the parameter return this.usersService.findOne(id); } // PUT /api/users/:id - Full update @Put(':id') async update( @Param('id', ParseIntPipe) id: number, @Body() updateUserDto: UpdateUserDto, ): Promise { return this.usersService.update(id, updateUserDto); } // DELETE /api/users/:id - Deletion @Delete(':id') @HttpCode(HttpStatus.NO_CONTENT) async remove(@Param('id', ParseIntPipe) id: number): Promise { await this.usersService.remove(id); } } ``` > **Validation Pipes integrate** > > La `ParseIntPipe` converte automaticamente i parametri stringa in numeri e restituisce un errore 400 se il valore non e' un numero valido. NestJS offre altre pipe integrate come `ParseBoolPipe`, `ParseArrayPipe` e `ParseUUIDPipe` per le conversioni piu' comuni. ## Implementazione del service di business Il service contiene tutta la logica di business ed e' contrassegnato come provider dal decorator `@Injectable()`. In questa prima versione, un array in memoria funge da archivio dati, prima dell'integrazione con Prisma nelle sezioni successive. ```typescript // src/users/users.service.ts import { Injectable, NotFoundException } from '@nestjs/common'; import { CreateUserDto } from './dto/create-user.dto'; import { UpdateUserDto } from './dto/update-user.dto'; import { User } from './entities/user.entity'; @Injectable() export class UsersService { // In-memory database simulation private users: User[] = []; private idCounter = 1; async create(createUserDto: CreateUserDto): Promise { // Create the user entity const user: User = { id: this.idCounter++, ...createUserDto, createdAt: new Date(), updatedAt: new Date(), }; this.users.push(user); return user; } async findAll(page: number, limit: number): Promise<{ data: User[]; total: number }> { // Calculate pagination const start = (page - 1) * limit; const end = start + limit; return { data: this.users.slice(start, end), total: this.users.length, }; } async findOne(id: number): Promise { const user = this.users.find(u => u.id === id); // Throw exception if user doesn't exist if (!user) { throw new NotFoundException(`User with ID ${id} not found`); } return user; } async update(id: number, updateUserDto: UpdateUserDto): Promise { const user = await this.findOne(id); // Merge existing data with updates Object.assign(user, updateUserDto, { updatedAt: new Date() }); return user; } async remove(id: number): Promise { const index = this.users.findIndex(u => u.id === id); if (index === -1) { throw new NotFoundException(`User with ID ${id} not found`); } this.users.splice(index, 1); } // Utility method for other services async findByEmail(email: string): Promise { return this.users.find(u => u.email === email); } } ``` Il service utilizza `NotFoundException` da `@nestjs/common` per generare errori 404 conformi allo standard HTTP. NestJS converte automaticamente queste eccezioni nelle risposte HTTP appropriate. ## Validazione dei dati con class-validator I DTO (Data Transfer Object) definiscono la struttura dei dati in ingresso e utilizzano i decorator di `class-validator` per la validazione. Il `CreateUserDto` specifica quali campi sono obbligatori per la creazione di un utente. ```typescript // src/users/dto/create-user.dto.ts import { IsEmail, IsNotEmpty, IsString, MinLength, MaxLength, IsOptional, Matches, } from 'class-validator'; export class CreateUserDto { @IsNotEmpty({ message: 'Email is required' }) @IsEmail({}, { message: 'Invalid email format' }) email: string; @IsNotEmpty({ message: 'Password is required' }) @MinLength(8, { message: 'Password must be at least 8 characters' }) @MaxLength(50, { message: 'Password cannot exceed 50 characters' }) @Matches( /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/, { message: 'Password must contain at least one uppercase, one lowercase, and one number' } ) password: string; @IsNotEmpty({ message: 'First name is required' }) @IsString() @MinLength(2) @MaxLength(50) firstName: string; @IsNotEmpty({ message: 'Last name is required' }) @IsString() @MinLength(2) @MaxLength(50) lastName: string; @IsOptional() @IsString() @MaxLength(20) phone?: string; } ``` L'`UpdateUserDto` utilizza `PartialType` e `OmitType` per rendere tutti i campi opzionali e per escludere la password dagli aggiornamenti standard. ```typescript // src/users/dto/update-user.dto.ts import { PartialType, OmitType } from '@nestjs/mapped-types'; import { CreateUserDto } from './create-user.dto'; // All fields from CreateUserDto become optional // Password is excluded from standard updates export class UpdateUserDto extends PartialType( OmitType(CreateUserDto, ['password'] as const) ) {} ``` L'entita' User definisce la struttura dati completa, inclusi i campi generati automaticamente. ```typescript // src/users/entities/user.entity.ts export class User { id: number; email: string; password: string; firstName: string; lastName: string; phone?: string; createdAt: Date; updatedAt: Date; } ``` ## Gestione centralizzata degli errori Gli exception filter intercettano gli errori e formattano le risposte HTTP in modo uniforme. L'`HttpExceptionFilter` gestisce gli errori HTTP noti, mentre l'`AllExceptionsFilter` cattura gli errori di sistema imprevisti. ```typescript // src/common/filters/http-exception.filter.ts import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus, } from '@nestjs/common'; import { Request, Response } from 'express'; // Catches all HttpExceptions @Catch(HttpException) export class HttpExceptionFilter implements ExceptionFilter { catch(exception: HttpException, host: ArgumentsHost) { const ctx = host.switchToHttp(); const response = ctx.getResponse(); const request = ctx.getRequest(); const status = exception.getStatus(); // Retrieve error message (can be string or object) const exceptionResponse = exception.getResponse(); const message = typeof exceptionResponse === 'string' ? exceptionResponse : (exceptionResponse as any).message; // Standardized response format response.status(status).json({ success: false, statusCode: status, timestamp: new Date().toISOString(), path: request.url, method: request.method, message: message, }); } } ``` ```typescript // src/common/filters/all-exceptions.filter.ts import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus, } from '@nestjs/common'; import { Request, Response } from 'express'; // Catches ALL exceptions (including system errors) @Catch() export class AllExceptionsFilter implements ExceptionFilter { catch(exception: unknown, host: ArgumentsHost) { const ctx = host.switchToHttp(); const response = ctx.getResponse(); const request = ctx.getRequest(); // Determine HTTP code and message const status = exception instanceof HttpException ? exception.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR; const message = exception instanceof HttpException ? exception.message : 'Internal server error'; // Log error for debugging console.error('Exception caught:', exception); response.status(status).json({ success: false, statusCode: status, timestamp: new Date().toISOString(), path: request.url, message: message, }); } } ``` Entrambi i filter vengono registrati globalmente nel file `main.ts`. ```typescript // src/main.ts (updated) import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { ValidationPipe } from '@nestjs/common'; import { HttpExceptionFilter } from './common/filters/http-exception.filter'; import { AllExceptionsFilter } from './common/filters/all-exceptions.filter'; async function bootstrap() { const app = await NestFactory.create(AppModule); // Global validation app.useGlobalPipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true, })); // Global exception filters app.useGlobalFilters( new AllExceptionsFilter(), new HttpExceptionFilter(), ); app.setGlobalPrefix('api'); await app.listen(3000); } bootstrap(); ``` > **L'ordine dei filter e' importante** > > L'ordine dei filter in `useGlobalFilters()` e' determinante. L'ultimo filter registrato viene eseguito per primo. L'`HttpExceptionFilter` deve essere posizionato dopo l'`AllExceptionsFilter`, in modo che le eccezioni HTTP vengano gestite in modo specifico prima che intervenga il filter generico. ## Integrazione con Prisma ORM Prisma offre un accesso al database type-safe con tipi generati automaticamente. L'installazione e la configurazione con PostgreSQL richiedono pochi passaggi. ```bash # terminal # Install Prisma npm install prisma @prisma/client # Initialize Prisma with PostgreSQL npx prisma init --datasource-provider postgresql ``` Lo schema Prisma definisce i modelli del database e le relative relazioni. ```prisma // prisma/schema.prisma generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } model User { id Int @id @default(autoincrement()) email String @unique password String firstName String @map("first_name") lastName String @map("last_name") phone String? createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") // Relations posts Post[] @@map("users") } model Post { id Int @id @default(autoincrement()) title String content String published Boolean @default(false) authorId Int @map("author_id") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") // Relation to User author User @relation(fields: [authorId], references: [id]) @@map("posts") } ``` Il `PrismaService` incapsula il client Prisma e gestisce la connessione al database tramite i lifecycle hook di NestJS. ```typescript // src/prisma/prisma.service.ts import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common'; import { PrismaClient } from '@prisma/client'; @Injectable() export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy { // Automatic connection on module startup async onModuleInit() { await this.$connect(); } // Clean disconnection on application shutdown async onModuleDestroy() { await this.$disconnect(); } } ``` Il `PrismaModule` e' decorato con `@Global()` per rendere il `PrismaService` disponibile in tutta l'applicazione senza doverlo importare in ogni singolo modulo. ```typescript // src/prisma/prisma.module.ts import { Global, Module } from '@nestjs/common'; import { PrismaService } from './prisma.service'; // @Global makes the service available throughout the application @Global() @Module({ providers: [PrismaService], exports: [PrismaService], }) export class PrismaModule {} ``` Il `UsersService` aggiornato sostituisce l'archivio in memoria con query Prisma e aggiunge l'hashing delle password e la verifica dell'unicita' dell'email. ```typescript // src/users/users.service.ts (Prisma version) import { Injectable, NotFoundException, ConflictException } from '@nestjs/common'; import { PrismaService } from '../prisma/prisma.service'; import { CreateUserDto } from './dto/create-user.dto'; import { UpdateUserDto } from './dto/update-user.dto'; import { User } from '@prisma/client'; import * as bcrypt from 'bcrypt'; @Injectable() export class UsersService { constructor(private readonly prisma: PrismaService) {} async create(createUserDto: CreateUserDto): Promise> { // Check email uniqueness const existingUser = await this.prisma.user.findUnique({ where: { email: createUserDto.email }, }); if (existingUser) { throw new ConflictException('This email is already in use'); } // Hash the password const hashedPassword = await bcrypt.hash(createUserDto.password, 10); // Create the user const user = await this.prisma.user.create({ data: { ...createUserDto, password: hashedPassword, }, }); // Exclude password from response const { password, ...result } = user; return result; } async findAll(page: number, limit: number): Promise<{ data: User[]; total: number }> { // Parallel execution of count and paginated query const [data, total] = await Promise.all([ this.prisma.user.findMany({ skip: (page - 1) * limit, take: limit, orderBy: { createdAt: 'desc' }, select: { id: true, email: true, firstName: true, lastName: true, phone: true, createdAt: true, updatedAt: true, }, }), this.prisma.user.count(), ]); return { data: data as User[], total }; } async findOne(id: number): Promise> { const user = await this.prisma.user.findUnique({ where: { id }, select: { id: true, email: true, firstName: true, lastName: true, phone: true, createdAt: true, updatedAt: true, }, }); if (!user) { throw new NotFoundException(`User with ID ${id} not found`); } return user as Omit; } async update(id: number, updateUserDto: UpdateUserDto): Promise> { // Check existence await this.findOne(id); const user = await this.prisma.user.update({ where: { id }, data: updateUserDto, select: { id: true, email: true, firstName: true, lastName: true, phone: true, createdAt: true, updatedAt: true, }, }); return user as Omit; } async remove(id: number): Promise { await this.findOne(id); await this.prisma.user.delete({ where: { id } }); } } ``` ## Interceptor per la trasformazione delle risposte Gli interceptor elaborano richieste e risposte in modo trasversale. Il `TransformInterceptor` racchiude tutte le risposte in un formato uniforme, mentre il `LoggingInterceptor` registra i tempi di esecuzione. ```typescript // src/common/interceptors/transform.interceptor.ts import { Injectable, NestInterceptor, ExecutionContext, CallHandler, } from '@nestjs/common'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; // Interface for standardized response format export interface ApiResponse { success: boolean; 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(), })), ); } } ``` ```typescript // 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(LoggingInterceptor.name); intercept(context: ExecutionContext, next: CallHandler): Observable { const request = context.switchToHttp().getRequest(); const { method, url } = request; const now = Date.now(); return next.handle().pipe( tap(() => { const response = context.switchToHttp().getResponse(); const { statusCode } = response; const duration = Date.now() - now; // Structured log format this.logger.log( `${method} ${url} ${statusCode} - ${duration}ms` ); }), ); } } ``` Il `TransformInterceptor` garantisce che tutti gli endpoint utilizzino lo stesso formato di risposta: `{ success, data, timestamp }`. Il `LoggingInterceptor` misura la durata di ogni richiesta e produce log strutturati, utili per il monitoraggio in produzione. ## Conclusione Questa guida ha trattato tutti i concetti fondamentali necessari per costruire una REST API pronta per la produzione con NestJS. Dall'architettura modulare alla validazione fino all'integrazione con il database, NestJS offre un ecosistema completo e coerente. ### Checklist per la REST API - Creare un progetto NestJS con la CLI e configurare la `ValidationPipe` globale - Suddividere il codice in moduli funzionali con separazione netta tra controller e service - Definire DTO con decorator `class-validator` per la validazione automatica dei dati - Implementare la gestione centralizzata degli errori con exception filter - Integrare Prisma per l'accesso type-safe al database - Implementare interceptor per formati di risposta uniformi e logging Partendo da questa base, l'API puo' essere estesa con guard per l'autenticazione, Swagger per la documentazione e middleware per il rate limiting. L'architettura modulare di NestJS garantisce che il codice rimanga organizzato e manutenibile anche con l'aumento della complessita'. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/it/blog/node-nestjs/nestjs-building-complete-rest-api