Se stai sviluppando con Next.js e ogni volta che interroghi il database finisci a controllare a mano se hai scritto bene il nome di una colonna, se il tipo è giusto, se la relazione esiste, il problema non è la tua attenzione. È l'assenza di un layer che unisca il frontend e il database in modo sicuro, senza buchi tra i tipi. Noi, di Meteora Web, abbiamo visto troppi progetti rallentare per errori stupidi: una query che restituisce undefined perché il campo si chiama created_at e non createdAt. Con Prisma e Next.js questo non succede. Il database diventa un cittadino di prima classe nel tuo stack TypeScript. Ogni tabella, ogni relazione, ogni tipo è generato automaticamente e controllato dal compilatore. Se sbagli, TypeScript ti ferma prima che il bug arrivi in produzione. In questa guida vediamo come configurare Prisma in un progetto Next.js con App Router, come fare query type-safe dentro i Server Components e come gestire mutazioni, senza rinunciare alla potenza di SQL. Ma lo facciamo con i piedi per terra: parliamo di costi, di performance reali, di manutenzione che non diventa un incubo.
Perché usare Prisma con Next.js invece di un ORM classico?
Quando scegli un ORM per Next.js, hai essenzialmente tre opzioni: scrivere SQL puro, usare un ORM classico come TypeORM o Sequelize, o adottare Prisma. Noi abbiamo provato tutte e tre su progetti veri. Il problema degli ORM classici? Il mapping manuale dei tipi. Ogni volta che cambi una colonna, devi aggiornare le interfacce TypeScript a mano. Un errore di distrazione e il type-checking ti dà un falso positivo o, peggio, non ti dice nulla fino a runtime. Prisma risolve questo generando automaticamente un client TypeScript a partire dallo schema del database. Il tuo schema schema.prisma diventa l'unica fonte di verità. Modifichi lì, lanci prisma generate, e tutti i tipi si aggiornano. Zero errori a runtime per mismatch di campo. E poi c'è la produttività: le query sono scritte con un'API dichiarativa che sembra quasi pseudo-codice, ma è type-safe dall'inizio alla fine.
Sponsored Protocol
Esempio pratico: query con relazione
Immagina di avere un modello User con una relazione posts. In Prisma scrivi:
const userWithPosts = await prisma.user.findUnique({
where: { id: userId },
include: { posts: true }
});
// user.posts è tipizzato come Post[]
// Se provi ad accedere a user.posts[0].title, TypeScript sa che title esiste ed è stringa
Se usassi SQL puro, dovresti destrutturare il risultato e castearlo manualmente. Con un ORM classico, dovresti definire un tipo a parte. Con Prisma, è automatico. Noi l'abbiamo usato per un'applicazione di gestione ordini con oltre 30 tabelle: i tempi di sviluppo delle nuove feature si sono ridotti del 40% perché il team non perdeva più tempo a cacciare errori di tipo tra API e database.
Come configurare Prisma in un progetto Next.js con App Router?
La configurazione è semplice, ma ci sono accorgimenti per non incappare in problemi di connessione o di build. Partiamo dall'installazione:
Sponsored Protocol
npm install prisma @prisma/client
npx prisma init
Questo crea il file prisma/schema.prisma e il .env con la stringa di connessione. Poi definisci lo schema. Un esempio base per un blog:
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
model User {
id String @id @default(cuid())
email String @unique
name String?
posts Post[]
}
model Post {
id String @id @default(cuid())
title String
content String?
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId String
}
Poi esegui la migrazione:
npx prisma migrate dev --name init
Ora devi creare un'istanza del client Prisma che possa essere riutilizzata in tutto il progetto senza creare nuove connessioni a ogni richiesta. In Next.js, il pattern standard è creare un singleton in lib/prisma.ts:
import { PrismaClient } from '@prisma/client'
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined
}
export const prisma = globalForPrisma.prisma ?? new PrismaClient()
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
Questo evita di eccedere il limite di connessioni in sviluppo (dove il server ricarica spesso) e in produzione mantiene un'unica connessione per processo. Lo abbiamo visto su un cliente che con una configurazione sbagliata consumava 200 connessioni su un database PostgreSQL con limite a 100. Con il singleton, risolto.
Sponsored Protocol
Query nei Server Components
In Next.js App Router, puoi interrogare il database direttamente nei Server Components. Ecco un esempio di pagina che mostra un utente con i suoi post:
// app/users/[id]/page.tsx
export default async function UserPage({ params }: { params: { id: string } }) {
const user = await prisma.user.findUnique({
where: { id: params.id },
include: { posts: true }
})
if (!user) return Utente non trovato
return (
{user.name}
{user.posts.map(post => (
- {post.title}
))}
)
}
Niente API route, niente useEffect, niente loading state a livello client. Il dato arriva già pronto nel server, la pagina è statica o dinamicamente renderizzata a seconda della revalidate. Questo è il vero full-stack type-safe: dal database al JSX, tutto coerente.
Quali sono le best practice per le mutazioni con Prisma e Next.js?
Le scritture (create, update, delete) vanno gestite con Server Actions, una feature nativa di Next.js. Non serve più creare API route per ogni operazione. E con Prisma, le Server Actions sono type-safe come il resto. Esempio di azione per creare un post:
Sponsored Protocol
// app/actions.ts
'use server'
import { prisma } from '@/lib/prisma'
import { revalidatePath } from 'next/cache'
export async function createPost(formData: FormData) {
const title = formData.get('title') as string
const content = formData.get('content') as string
const authorId = formData.get('authorId') as string
// Validazione di base
if (!title || !authorId) throw new Error('Campi obbligatori mancanti')
await prisma.post.create({
data: { title, content, authorId }
})
revalidatePath('/posts')
}
Poi nel componente client chiami questa azione:
// app/posts/new/page.tsx
'use client'
import { createPost } from '@/app/actions'
export default function NewPostForm() {
return (
)
}
Nessuna chiamata fetch, nessun endpoint esposto, e il tipo del risultato è garantito dal client Prisma. Attenzione: la validazione lato server è fondamentale. Noi aggiungiamo sempre un layer di validazione con Zod o manuale prima di chiamare Prisma, per evitare dati malformati.
Come gestire le performance delle query in Next.js con Prisma?
Prisma è comodo, ma se non stai attento può generare query N+1. Per esempio, se in un Server Component fai una query per ottenere tutti gli utenti e poi per ogni utente fai una query separata per i suoi post, hai un N+1. Invece, usa include o select per fare eager loading.
// Male: N+1
const users = await prisma.user.findMany()
for (const user of users) {
const posts = await prisma.post.findMany({ where: { authorId: user.id } })
}
// Bene: eager loading
const usersWithPosts = await prisma.user.findMany({
include: { posts: true }
})
Un altro punto: Prisma di default esegue una transazione per ogni query. Se devi fare più operazioni correlate, usa prisma.$transaction per raggrupparle atomicamente. E per operazioni bulk, createMany e updateMany sono molto più efficienti di loop individuali. Noi abbiamo ottimizzato un import di 50.000 record da un CSV: passare da create in loop a createMany ha ridotto il tempo da 12 minuti a 3 secondi.
Sponsored Protocol
Cosa fare adesso
- Configura Prisma nel tuo progetto Next.js seguendo i passaggi sopra: installa, definisci lo schema, crea il singleton.
- Sostituisci almeno una query esistente con un Server Component che usa Prisma direttamente. Vedrai subito la differenza di chiarezza.
- Converti una API route in Server Action per una mutazione. Rimuovi il codice client di fetch e usa l'action nel form.
- Controlla le performance con i log di Prisma: attiva
logging: ['query']nel client per vedere quante query esegui per pagina e ottimizza conincludeoselect. - Leggi la documentazione ufficiale di Prisma Client CRUD e di Next.js Server Actions.
Per un approfondimento su tutto lo stack Next.js, leggi il nostro pillar su Next.js App Router.