Drizzle ORM nel 2026: Type-Safe Database Queries in TypeScript

Prisma ha dominato il panorama degli ORM TypeScript per anni, ma nel 2026 Drizzle ORM è diventato la scelta preferita per i developer che vogliono type-safety totale, query builder SQL-like senza magia implicita e performance superiori. La differenza principale: Drizzle non genera query al volo — le costruisce in modo prevedibile e statico, con un’esperienza di debug molto più semplice. In questa guida costruiamo un’applicazione completa con Drizzle, PostgreSQL su Supabase e Next.js.

Perché Drizzle invece di Prisma

Le differenze principali che spingono i developer a migrare da Prisma a Drizzle nel 2026:

Bundle size: Drizzle è 10-50x più leggero di Prisma — fondamentale per Edge Functions e serverless. Prisma genera e include un query engine binario nella build; Drizzle è puro JavaScript/TypeScript. Query SQL trasparenti: Drizzle scrive query che puoi leggere e capire. Non c’è una “magia” che trasforma le operazioni Prisma in SQL — quello che scrivi è quasi esattamente quello che viene eseguito. Type inference migliore: lo schema Drizzle è scritto in TypeScript puro, non in un DSL separato (schema.prisma). I tipi vengono inferiti direttamente dalla definizione della tabella, senza generazione di codice. Migliori sugli Edge: Cloudflare Workers, Vercel Edge, Supabase Edge Functions — tutti hanno limitazioni sui binari che rendono Prisma difficile. Drizzle funziona ovunque giri JavaScript.

Installazione e configurazione

npm install drizzle-orm postgres
npm install -D drizzle-kit @types/pg

Configura la connessione (db/index.ts):

import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
import * as schema from "./schema";

const connectionString = process.env.DATABASE_URL!;

// Per produzione: pool di connessioni
const client = postgres(connectionString, { max: 10 });
export const db = drizzle(client, { schema });

Definire lo schema con pgTable

Lo schema in Drizzle è TypeScript puro — nessun file .prisma, nessuna generazione di codice:

// db/schema.ts
import {
  pgTable, serial, text, integer, boolean,
  timestamp, uuid, varchar, decimal, index, foreignKey,
} from "drizzle-orm/pg-core";

export const users = pgTable("users", {
  id:        uuid("id").primaryKey().defaultRandom(),
  email:     varchar("email", { length: 255 }).notNull().unique(),
  name:      text("name").notNull(),
  plan:      text("plan", { enum: ["free", "pro", "team"] }).default("free"),
  createdAt: timestamp("created_at").defaultNow().notNull(),
  updatedAt: timestamp("updated_at").defaultNow().notNull(),
});

export const projects = pgTable("projects", {
  id:          serial("id").primaryKey(),
  userId:      uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
  name:        varchar("name", { length: 100 }).notNull(),
  description: text("description"),
  isPublic:    boolean("is_public").default(false).notNull(),
  createdAt:   timestamp("created_at").defaultNow().notNull(),
}, (table) => ({
  userIdIdx: index("projects_user_id_idx").on(table.userId),
}));

export const tasks = pgTable("tasks", {
  id:          serial("id").primaryKey(),
  projectId:   integer("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
  title:       text("title").notNull(),
  status:      text("status", { enum: ["todo", "in_progress", "done"] }).default("todo"),
  priority:    integer("priority").default(0),
  dueDate:     timestamp("due_date"),
  createdAt:   timestamp("created_at").defaultNow().notNull(),
});

// Tipi TypeScript inferiti automaticamente
export type User    = typeof users.$inferSelect;
export type Project = typeof projects.$inferSelect;
export type Task    = typeof tasks.$inferSelect;
export type NewTask = typeof tasks.$inferInsert;

Select: query type-safe

import { db } from "@/db";
import { users, projects, tasks } from "@/db/schema";
import { eq, and, desc, gt, sql } from "drizzle-orm";

// Select semplice
const allUsers = await db.select().from(users);
// Tipo inferito: User[]

// Select con filtro
const user = await db
  .select()
  .from(users)
  .where(eq(users.email, "mario@example.com"))
  .limit(1);

// Select con join
const projectsWithTasks = await db
  .select({
    projectId:   projects.id,
    projectName: projects.name,
    taskCount:   sql<number>`COUNT(${tasks.id})`,
  })
  .from(projects)
  .leftJoin(tasks, eq(projects.id, tasks.projectId))
  .where(eq(projects.userId, userId))
  .groupBy(projects.id, projects.name)
  .orderBy(desc(projects.createdAt));

// Query relazionale (con schema definito)
const usersWithProjects = await db.query.users.findMany({
  where: eq(users.plan, "pro"),
  with: {
    projects: {
      where: eq(projects.isPublic, true),
      orderBy: desc(projects.createdAt),
      limit: 5,
    },
  },
});

Insert, Update, Delete

// INSERT — con returning per avere i dati inseriti
const [newProject] = await db
  .insert(projects)
  .values({
    userId:      userId,
    name:        "Nuovo progetto",
    description: "Descrizione",
    isPublic:    false,
  })
  .returning();
// newProject: Project (tipo inferito)

// INSERT multiplo
await db.insert(tasks).values([
  { projectId: newProject.id, title: "Task 1", status: "todo" },
  { projectId: newProject.id, title: "Task 2", status: "in_progress" },
]);

// UPDATE
const [updated] = await db
  .update(tasks)
  .set({
    status:    "done",
    updatedAt: new Date(),
  })
  .where(and(eq(tasks.id, taskId), eq(tasks.projectId, projectId)))
  .returning();

// DELETE con returning
const [deleted] = await db
  .delete(projects)
  .where(eq(projects.id, projectId))
  .returning({ id: projects.id, name: projects.name });

// Upsert (insert or update)
await db
  .insert(users)
  .values({ email, name, plan: "free" })
  .onConflictDoUpdate({
    target: users.email,
    set: { name, updatedAt: new Date() },
  });

Migrations con drizzle-kit

Drizzle-kit confronta il tuo schema TypeScript con lo stato attuale del database e genera i file SQL di migrazione. Configura drizzle.config.ts:

// drizzle.config.ts
import { defineConfig } from "drizzle-kit";

export default defineConfig({
  schema:    "./db/schema.ts",
  out:       "./db/migrations",
  dialect:   "postgresql",
  dbCredentials: {
    url: process.env.DATABASE_URL!,
  },
  verbose: true,
  strict:  true,
});

Workflow di sviluppo:

# Genera il file di migrazione SQL dallo schema
npx drizzle-kit generate

# Applica le migrazioni al database
npx drizzle-kit migrate

# Apri Drizzle Studio (GUI database nel browser)
npx drizzle-kit studio

# Push diretto senza migration file (solo sviluppo)
npx drizzle-kit push

I file di migrazione generati sono SQL standard leggibili — puoi ispezionarli e modificarli prima di applicarli. In produzione, esegui le migrazioni come step del tuo deployment pipeline.

Integrazione con Next.js App Router

Drizzle funziona perfettamente nei Server Components e nelle Server Actions di Next.js. Esempio di Server Action per creare un task:

// app/actions/tasks.ts
"use server";

import { db } from "@/db";
import { tasks } from "@/db/schema";
import { revalidatePath } from "next/cache";
import { z } from "zod";

const CreateTaskSchema = z.object({
  projectId: z.number().int().positive(),
  title:     z.string().min(1).max(200),
  priority:  z.number().int().min(0).max(3).optional(),
});

export async function createTask(formData: FormData) {
  const parsed = CreateTaskSchema.safeParse({
    projectId: Number(formData.get("projectId")),
    title:     formData.get("title"),
    priority:  Number(formData.get("priority") ?? 0),
  });

  if (!parsed.success) {
    return { error: parsed.error.flatten() };
  }

  const [task] = await db
    .insert(tasks)
    .values(parsed.data)
    .returning();

  revalidatePath(`/projects/${parsed.data.projectId}`);
  return { task };
}

Risorse correlate

Approfondisci sul blog

Documentazione e strumenti

FAQ

Drizzle funziona con database diversi da PostgreSQL?

Sì. Drizzle supporta PostgreSQL, MySQL, SQLite e altri. L’API è quasi identica tra i dialect — cambia principalmente l’import (drizzle-orm/mysql2, drizzle-orm/better-sqlite3) e alcune funzioni specifiche del database. Per SQLite in ambiente serverless (Cloudflare D1, Turso), Drizzle è la scelta di riferimento.

Come migro da Prisma a Drizzle?

La migrazione non è automatizzata ma è gestibile. Il processo: (1) riscrivi lo schema schema.prisma in pgTable TypeScript, (2) genera una migrazione Drizzle che porta il database allo stato attuale (senza applicarla — il DB è già migrato da Prisma), (3) aggiorna le query una file alla volta. Non esiste un tool di migrazione automatica da Prisma a Drizzle nel 2026.

Drizzle ha un equivalente di Prisma Studio?

Sì: npx drizzle-kit studio apre una GUI web per esplorare e modificare i dati del database. Non è ricca come Prisma Studio in termini di funzionalità avanzate, ma copre i casi d’uso principali per il debug durante lo sviluppo.

Posso usare Drizzle con Supabase?

Sì, ed è una delle combinazioni più usate nel 2026. Usi Drizzle come query builder TypeScript sopra il database PostgreSQL di Supabase, e il client Supabase per auth, realtime e storage. Non conflicto — sono due layer separati che si completano. Connetti Drizzle direttamente alla connection string PostgreSQL di Supabase (non via REST API).

Condividi

Articoli Recenti

Categorie popolari