# NestJS + Prisma: lo stack backend moderno per Node.js > Guida completa per costruire un'API backend moderna con NestJS e Prisma. Configurazione, modelli, servizi, transazioni e best practice spiegate. - Published: 2026-02-09 - Updated: 2026-04-28 - Author: SharpSkill - Tags: nestjs, prisma, nodejs, typescript, backend - Reading time: 14 min --- NestJS e Prisma formano una combinazione potente per lo sviluppo backend moderno. NestJS offre un'architettura modulare e l'iniezione delle dipendenze, mentre Prisma fornisce un ORM tipizzato con un'esperienza di sviluppo eccezionale. Questo stack consente di costruire API robuste e facili da mantenere. > **Perché questa combinazione** > > Prisma genera automaticamente un client TypeScript tipizzato a partire dallo schema del database. Combinato con NestJS e il suo sistema di moduli, il codice si autodocumenta e gli errori di tipo vengono rilevati in fase di compilazione. ## Configurazione iniziale del progetto Integrare Prisma in un progetto NestJS richiede alcuni passaggi di configurazione. Il processo è standard e ben documentato. ```bash # terminal # Create a new NestJS project nest new my-backend-api cd my-backend-api # Install Prisma as a dev dependency npm install prisma --save-dev # Install the Prisma client npm install @prisma/client # Initialize Prisma with PostgreSQL npx prisma init --datasource-provider postgresql ``` Questo comando crea una cartella `prisma/` con il file `schema.prisma` e un file `.env` per le variabili d'ambiente. ```typescript // src/prisma/prisma.service.ts import { Injectable, OnModuleInit, OnModuleDestroy, Logger } from '@nestjs/common'; import { PrismaClient } from '@prisma/client'; @Injectable() export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy { // Dedicated logger for Prisma operations private readonly logger = new Logger(PrismaService.name); constructor() { // Configure client with logging in development super({ log: process.env.NODE_ENV === 'development' ? ['query', 'info', 'warn', 'error'] : ['error'], }); } // Automatic connection on module startup async onModuleInit() { await this.$connect(); this.logger.log('Prisma connection established'); } // Clean disconnection on application shutdown async onModuleDestroy() { await this.$disconnect(); this.logger.log('Prisma connection closed'); } } ``` Il servizio Prisma è ora pronto per essere iniettato negli altri servizi dell'applicazione. ## Creare il modulo Prisma globale Per rendere il servizio Prisma disponibile in tutta l'applicazione senza importarlo esplicitamente in ogni modulo, si utilizza il decoratore `@Global()`. ```typescript // src/prisma/prisma.module.ts import { Global, Module } from '@nestjs/common'; import { PrismaService } from './prisma.service'; // The @Global decorator makes this module available everywhere @Global() @Module({ providers: [PrismaService], exports: [PrismaService], }) export class PrismaModule {} ``` L'importazione nel modulo radice rende il servizio automaticamente disponibile. ```typescript // src/app.module.ts import { Module } from '@nestjs/common'; import { PrismaModule } from './prisma/prisma.module'; import { UsersModule } from './users/users.module'; import { PostsModule } from './posts/posts.module'; @Module({ imports: [ PrismaModule, // Single declaration is sufficient UsersModule, PostsModule, ], }) export class AppModule {} ``` Ora qualsiasi servizio può iniettare `PrismaService` senza ulteriori importazioni. > **Modulo globale vs importazioni esplicite** > > Un modulo globale semplifica l'architettura ma rende implicite le dipendenze. Per applicazioni di piccole dimensioni questo è accettabile. Nei progetti grandi, le importazioni esplicite migliorano la tracciabilità delle dipendenze. ## Definire lo schema Prisma Il file `schema.prisma` definisce i modelli di dati, le relazioni e le opzioni del database. Prisma utilizza il proprio linguaggio di definizione dello schema (PSL). ```prisma // prisma/schema.prisma generator client { provider = "prisma-client-js" // Enable types for advanced filtering queries previewFeatures = ["fullTextSearch"] } datasource db { provider = "postgresql" url = env("DATABASE_URL") } // User model with all relations model User { id String @id @default(cuid()) email String @unique password String name String role Role @default(USER) createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") // One-to-many relations posts Post[] comments Comment[] profile Profile? // Index for frequent searches @@index([email]) @@map("users") } // Enum for user roles enum Role { USER ADMIN MODERATOR } // One-to-one relation with User model Profile { id String @id @default(cuid()) bio String? avatar String? website String? userId String @unique @map("user_id") user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@map("profiles") } // Posts with many-to-one relation to User model Post { id String @id @default(cuid()) title String slug String @unique content String excerpt String? published Boolean @default(false) publishedAt DateTime? @map("published_at") authorId String @map("author_id") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") // Relations author User @relation(fields: [authorId], references: [id], onDelete: Cascade) comments Comment[] categories CategoriesOnPosts[] @@index([authorId]) @@index([slug]) @@map("posts") } // Comments with dual relation model Comment { id String @id @default(cuid()) content String postId String @map("post_id") authorId String @map("author_id") createdAt DateTime @default(now()) @map("created_at") post Post @relation(fields: [postId], references: [id], onDelete: Cascade) author User @relation(fields: [authorId], references: [id], onDelete: Cascade) @@index([postId]) @@map("comments") } // Categories for posts model Category { id String @id @default(cuid()) name String @unique slug String @unique posts CategoriesOnPosts[] @@map("categories") } // Pivot table for many-to-many relation model CategoriesOnPosts { postId String @map("post_id") categoryId String @map("category_id") assignedAt DateTime @default(now()) @map("assigned_at") post Post @relation(fields: [postId], references: [id], onDelete: Cascade) category Category @relation(fields: [categoryId], references: [id], onDelete: Cascade) @@id([postId, categoryId]) @@map("categories_on_posts") } ``` Dopo aver modificato lo schema, le migrazioni applicano le modifiche al database. ```bash # terminal # Create a migration with descriptive name npx prisma migrate dev --name init_schema # Generate Prisma client (automatic after migrate dev) npx prisma generate # View schema in browser npx prisma studio ``` ## Implementare il servizio utenti con Prisma Il servizio Users illustra le operazioni CRUD comuni con Prisma. La tipizzazione automatica garantisce coerenza tra il codice e il database. ```typescript // src/users/users.service.ts import { Injectable, NotFoundException, ConflictException, BadRequestException, } 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, Prisma } from '@prisma/client'; import * as bcrypt from 'bcrypt'; // Type for results without password type SafeUser = Omit; @Injectable() export class UsersService { constructor(private readonly prisma: PrismaService) {} // Default selection without password private readonly safeSelect: Prisma.UserSelect = { id: true, email: true, name: true, role: true, createdAt: true, updatedAt: true, profile: true, }; 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'); } // Secure password hashing const hashedPassword = await bcrypt.hash(createUserDto.password, 12); // Creation with optional profile return this.prisma.user.create({ data: { email: createUserDto.email, password: hashedPassword, name: createUserDto.name, // Nested profile creation if provided profile: createUserDto.bio ? { create: { bio: createUserDto.bio, }, } : undefined, }, select: this.safeSelect, }); } async findAll(params: { page?: number; limit?: number; search?: string; }): Promise<{ data: SafeUser[]; total: number; pages: number }> { const { page = 1, limit = 10, search } = params; const skip = (page - 1) * limit; // Optional search condition const where: Prisma.UserWhereInput = search ? { OR: [ { email: { contains: search, mode: 'insensitive' } }, { name: { contains: search, mode: 'insensitive' } }, ], } : {}; // Parallel execution for performance const [data, total] = await this.prisma.$transaction([ this.prisma.user.findMany({ where, skip, take: limit, orderBy: { createdAt: 'desc' }, select: this.safeSelect, }), this.prisma.user.count({ where }), ]); return { data, total, pages: Math.ceil(total / limit), }; } async findOne(id: string): Promise { const user = await this.prisma.user.findUnique({ where: { id }, select: { ...this.safeSelect, // Include recent posts posts: { take: 5, orderBy: { createdAt: 'desc' }, select: { id: true, title: true, slug: true, published: true, }, }, }, }); if (!user) { throw new NotFoundException(`User with ID ${id} not found`); } return user; } async update(id: string, updateUserDto: UpdateUserDto): Promise { // Check existence await this.findOne(id); // Update with nested profile handling return this.prisma.user.update({ where: { id }, data: { name: updateUserDto.name, // Update or create profile profile: updateUserDto.bio ? { upsert: { create: { bio: updateUserDto.bio }, update: { bio: updateUserDto.bio }, }, } : undefined, }, select: this.safeSelect, }); } async remove(id: string): Promise { await this.findOne(id); // Deletion cascades to profile and posts await this.prisma.user.delete({ where: { id } }); } } ``` La tipizzazione di Prisma garantisce che tutte le proprietà utilizzate esistano nello schema. ## Gestire le relazioni con Prisma Prisma semplifica la gestione di relazioni complesse. Le query annidate consentono di caricare i dati correlati in una sola richiesta. ```typescript // src/posts/posts.service.ts import { Injectable, NotFoundException, ForbiddenException } from '@nestjs/common'; import { PrismaService } from '../prisma/prisma.service'; import { CreatePostDto } from './dto/create-post.dto'; import { UpdatePostDto } from './dto/update-post.dto'; import { Prisma } from '@prisma/client'; @Injectable() export class PostsService { constructor(private readonly prisma: PrismaService) {} async create(authorId: string, createPostDto: CreatePostDto) { // Automatic slug generation from title const slug = this.generateSlug(createPostDto.title); return this.prisma.post.create({ data: { title: createPostDto.title, slug, content: createPostDto.content, excerpt: createPostDto.excerpt, // Connect to existing author author: { connect: { id: authorId }, }, // Connect to existing categories categories: createPostDto.categoryIds ? { create: createPostDto.categoryIds.map(categoryId => ({ category: { connect: { id: categoryId } }, })), } : undefined, }, include: { author: { select: { id: true, name: true }, }, categories: { include: { category: true, }, }, }, }); } async findAllPublished(params: { page?: number; limit?: number; categorySlug?: string; }) { const { page = 1, limit = 10, categorySlug } = params; const skip = (page - 1) * limit; // Conditional filter by category const where: Prisma.PostWhereInput = { published: true, ...(categorySlug && { categories: { some: { category: { slug: categorySlug }, }, }, }), }; const [posts, total] = await this.prisma.$transaction([ this.prisma.post.findMany({ where, skip, take: limit, orderBy: { publishedAt: 'desc' }, include: { author: { select: { id: true, name: true }, }, categories: { include: { category: { select: { name: true, slug: true } }, }, }, _count: { select: { comments: true }, }, }, }), this.prisma.post.count({ where }), ]); return { posts, total, pages: Math.ceil(total / limit) }; } async findBySlug(slug: string) { const post = await this.prisma.post.findUnique({ where: { slug }, include: { author: { select: { id: true, name: true, profile: true }, }, categories: { include: { category: true, }, }, comments: { orderBy: { createdAt: 'desc' }, take: 20, include: { author: { select: { id: true, name: true }, }, }, }, }, }); if (!post) { throw new NotFoundException(`Post "${slug}" not found`); } return post; } async publish(id: string, authorId: string) { // Verify author is the owner const post = await this.prisma.post.findUnique({ where: { id }, select: { authorId: true }, }); if (!post) { throw new NotFoundException(`Post with ID ${id} not found`); } if (post.authorId !== authorId) { throw new ForbiddenException('Publication not authorized'); } return this.prisma.post.update({ where: { id }, data: { published: true, publishedAt: new Date(), }, }); } private generateSlug(title: string): string { return title .toLowerCase() .normalize('NFD') .replace(/[\u0300-\u036f]/g, '') .replace(/[^a-z0-9]+/g, '-') .replace(/(^-|-$)/g, ''); } } ``` Gli `include` permettono di recuperare relazioni profonde controllando quali campi vengono restituiti. ## Transazioni e operazioni atomiche Prisma offre diversi metodi per garantire l'atomicità delle operazioni. Le transazioni interattive offrono la massima flessibilità. ```typescript // src/orders/orders.service.ts import { Injectable, BadRequestException } from '@nestjs/common'; import { PrismaService } from '../prisma/prisma.service'; import { CreateOrderDto } from './dto/create-order.dto'; @Injectable() export class OrdersService { constructor(private readonly prisma: PrismaService) {} async createOrder(userId: string, createOrderDto: CreateOrderDto) { // Interactive transaction to ensure atomicity return this.prisma.$transaction(async (tx) => { // 1. Verify stock for each item const items = await Promise.all( createOrderDto.items.map(async (item) => { const product = await tx.product.findUnique({ where: { id: item.productId }, }); if (!product) { throw new BadRequestException( `Product ${item.productId} not found` ); } if (product.stock < item.quantity) { throw new BadRequestException( `Insufficient stock for ${product.name}` ); } return { product, quantity: item.quantity }; }) ); // 2. Calculate total const total = items.reduce( (sum, { product, quantity }) => sum + product.price * quantity, 0 ); // 3. Create order const order = await tx.order.create({ data: { userId, total, status: 'PENDING', items: { create: items.map(({ product, quantity }) => ({ productId: product.id, quantity, price: product.price, })), }, }, include: { items: { include: { product: true }, }, }, }); // 4. Update stock await Promise.all( items.map(({ product, quantity }) => tx.product.update({ where: { id: product.id }, data: { stock: { decrement: quantity } }, }) ) ); return order; }); } async cancelOrder(orderId: string, userId: string) { return this.prisma.$transaction(async (tx) => { // Get order with items const order = await tx.order.findUnique({ where: { id: orderId }, include: { items: true }, }); if (!order || order.userId !== userId) { throw new BadRequestException('Order not found'); } if (order.status !== 'PENDING') { throw new BadRequestException( 'Only pending orders can be cancelled' ); } // Restore stock await Promise.all( order.items.map((item) => tx.product.update({ where: { id: item.productId }, data: { stock: { increment: item.quantity } }, }) ) ); // Update status return tx.order.update({ where: { id: orderId }, data: { status: 'CANCELLED' }, }); }); } } ``` Le transazioni garantiscono che tutte le operazioni vengano completate insieme o falliscano insieme. > **Timeout delle transazioni** > > Per impostazione predefinita, le transazioni Prisma hanno un timeout di 5 secondi. Per operazioni lunghe può essere modificato con `$transaction([...], { timeout: 10000 })`. ## Middleware Prisma per l'auditing I middleware Prisma permettono di intercettare le query per aggiungere comportamenti trasversali come l'auditing o il soft delete. ```typescript // src/prisma/prisma.service.ts (version with middleware) import { Injectable, OnModuleInit, OnModuleDestroy, Logger } from '@nestjs/common'; import { PrismaClient, Prisma } from '@prisma/client'; @Injectable() export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy { private readonly logger = new Logger(PrismaService.name); constructor() { super({ log: [ { level: 'query', emit: 'event' }, { level: 'error', emit: 'stdout' }, ], }); // Middleware for modification auditing this.$use(async (params: Prisma.MiddlewareParams, next) => { const start = Date.now(); // Execute query const result = await next(params); const duration = Date.now() - start; // Log slow queries (> 100ms) if (duration > 100) { this.logger.warn( `Slow query: ${params.model}.${params.action} - ${duration}ms` ); } // Audit write operations if (['create', 'update', 'delete'].includes(params.action)) { this.logger.log( `Audit: ${params.action} on ${params.model} - ${duration}ms` ); } return result; }); // Middleware for automatic soft delete this.$use(async (params, next) => { // Transform delete to update for certain models if (params.model === 'User' && params.action === 'delete') { params.action = 'update'; params.args['data'] = { deletedAt: new Date() }; } // Automatic exclusion of deleted records if (params.model === 'User' && params.action === 'findMany') { if (!params.args) params.args = {}; if (!params.args.where) params.args.where = {}; params.args.where.deletedAt = null; } return next(params); }); } async onModuleInit() { await this.$connect(); } async onModuleDestroy() { await this.$disconnect(); } } ``` I middleware vengono eseguiti nell'ordine di registrazione e possono modificare i parametri della query. ## Ottimizzazione delle prestazioni con Prisma Diverse tecniche ottimizzano le prestazioni delle query Prisma in un'applicazione NestJS. ```typescript // src/common/prisma-extensions.ts import { Prisma, PrismaClient } from '@prisma/client'; // Extension for standardized pagination export const paginationExtension = Prisma.defineExtension({ model: { $allModels: { async paginate( this: T, args: Prisma.Exact> & { page?: number; limit?: number; } ): Promise<{ data: Prisma.Result; meta: { page: number; limit: number; total: number; pages: number }; }> { const { page = 1, limit = 10, ...rest } = args as any; const skip = (page - 1) * limit; const context = Prisma.getExtensionContext(this); const [data, total] = await Promise.all([ (context as any).findMany({ ...rest, skip, take: limit }), (context as any).count({ where: (rest as any).where }), ]); return { data, meta: { page, limit, total, pages: Math.ceil(total / limit), }, }; }, }, }, }); // Usage in service // const result = await this.prisma.$extends(paginationExtension) // .user.paginate({ page: 2, limit: 20, where: { role: 'USER' } }); ``` Per le query frequenti, la cache migliora significativamente i tempi di risposta. ```typescript // src/posts/posts.service.ts (with cache) import { Injectable, Inject } from '@nestjs/common'; import { CACHE_MANAGER } from '@nestjs/cache-manager'; import { Cache } from 'cache-manager'; import { PrismaService } from '../prisma/prisma.service'; @Injectable() export class PostsService { constructor( private readonly prisma: PrismaService, @Inject(CACHE_MANAGER) private cacheManager: Cache, ) {} async findPopularPosts() { const cacheKey = 'posts:popular'; // Try to get from cache const cached = await this.cacheManager.get(cacheKey); if (cached) { return cached; } // Database query const posts = await this.prisma.post.findMany({ where: { published: true }, orderBy: { comments: { _count: 'desc' } }, take: 10, include: { author: { select: { name: true } }, _count: { select: { comments: true } }, }, }); // Cache for 5 minutes await this.cacheManager.set(cacheKey, posts, 300000); return posts; } async invalidateCache(postId: string) { // Selective cache invalidation await this.cacheManager.del('posts:popular'); await this.cacheManager.del(`post:${postId}`); } } ``` ## Test con Prisma e NestJS I test richiedono una strategia di database isolato. Utilizzare un database dedicato ai test garantisce la riproducibilità. ```typescript // test/helpers/prisma-test.helper.ts import { PrismaClient } from '@prisma/client'; import { execSync } from 'child_process'; const prisma = new PrismaClient(); export async function setupTestDatabase() { // Use a test database process.env.DATABASE_URL = process.env.TEST_DATABASE_URL; // Apply migrations execSync('npx prisma migrate deploy', { env: { ...process.env, DATABASE_URL: process.env.TEST_DATABASE_URL }, }); } export async function cleanupTestDatabase() { // Delete all data in dependency order const tablenames = await prisma.$queryRaw>` SELECT tablename FROM pg_tables WHERE schemaname='public' `; for (const { tablename } of tablenames) { if (tablename !== '_prisma_migrations') { await prisma.$executeRawUnsafe( `TRUNCATE TABLE "public"."${tablename}" CASCADE;` ); } } } export async function disconnectTestDatabase() { await prisma.$disconnect(); } ``` I test di integrazione utilizzano questi helper per garantire un ambiente pulito. ```typescript // test/users.e2e-spec.ts import { Test, TestingModule } from '@nestjs/testing'; import { INestApplication, ValidationPipe } from '@nestjs/common'; import * as request from 'supertest'; import { AppModule } from '../src/app.module'; import { PrismaService } from '../src/prisma/prisma.service'; import { cleanupTestDatabase, setupTestDatabase } from './helpers/prisma-test.helper'; describe('UsersController (e2e)', () => { let app: INestApplication; let prisma: PrismaService; beforeAll(async () => { await setupTestDatabase(); const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [AppModule], }).compile(); app = moduleFixture.createNestApplication(); app.useGlobalPipes(new ValidationPipe({ whitelist: true })); prisma = app.get(PrismaService); await app.init(); }); beforeEach(async () => { await cleanupTestDatabase(); }); afterAll(async () => { await app.close(); }); describe('POST /users', () => { it('should create a new user', async () => { const createUserDto = { email: 'test@example.com', password: 'Password123!', name: 'Test User', }; const response = await request(app.getHttpServer()) .post('/users') .send(createUserDto) .expect(201); expect(response.body).toMatchObject({ email: createUserDto.email, name: createUserDto.name, }); expect(response.body.password).toBeUndefined(); }); it('should reject duplicate email', async () => { const createUserDto = { email: 'duplicate@example.com', password: 'Password123!', name: 'First User', }; await request(app.getHttpServer()) .post('/users') .send(createUserDto) .expect(201); await request(app.getHttpServer()) .post('/users') .send({ ...createUserDto, name: 'Second User' }) .expect(409); }); }); }); ``` ## Conclusione NestJS e Prisma formano uno stack backend moderno e produttivo. La tipizzazione automatica, le migrazioni dichiarative e l'integrazione nativa con NestJS permettono di sviluppare API robuste rapidamente. ### Checklist per un'integrazione NestJS + Prisma riuscita - ✅ Modulo Prisma globale per un'iniezione semplificata - ✅ Schema Prisma con relazioni e indici ottimizzati - ✅ Servizi tipizzati con selezione esplicita dei campi - ✅ Transazioni per operazioni atomiche - ✅ Middleware per auditing e soft delete - ✅ Cache per le query frequenti - ✅ Test di integrazione con database isolato - ✅ Paginazione standardizzata tramite extension Questa combinazione sfrutta i punti di forza di entrambi gli strumenti: l'architettura modulare di NestJS per la struttura e Prisma per uno strato dati tipizzato. Il risultato è codice mantenibile, testabile e performante, adatto ad applicazioni enterprise. --- 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-prisma-modern-backend-stack