Bevezetés: Miért pont a Drizzle ORM a legjobb választás Next.js-hez 2026-ban?
Ha valaha is próbáltál adatbázist integrálni egy Next.js App Router projektbe, akkor pontosan tudod, milyen érzés elveszni az ORM-ek tengerében. Prisma, TypeORM, Kysely — a lista hosszú. De 2026-ban szerintem a Drizzle ORM egyértelműen kitűnik a mezőnyből. Könnyűsúlyú, típusbiztos, és natívan illeszkedik a Server Components meg a Server Actions világába. Nincs kódgenerálási lépés, nincs bináris fájl — csak tiszta TypeScript, ami úgy néz ki, mint az SQL, amit már ismersz.
Na de miért is mondom ezt ilyen magabiztosan?
Mert tényleg kipróbáltam éles projektben, és a különbség érezhető. Ebben az útmutatóban végigvezetlek a teljes folyamaton: a Drizzle ORM telepítésétől kezdve a séma definiáláson, migrációkon és Server Components lekérdezéseken át egészen a Server Actions CRUD műveletekig, Zod validációig és a produkciós connection pooling beállításáig. Minden kódrészlet működőképes és azonnal használható a saját projektedben.
Telepítés és projekt beállítás
Kezdjük a szükséges csomagok telepítésével. Feltételezem, hogy már van egy működő Next.js App Router projekted — ha még nincs, futtasd a npx create-next-app@latest parancsot TypeScript támogatással, és máris jöhetsz vissza.
npm install drizzle-orm @neondatabase/serverless
npm install -D drizzle-kit
Gyors összefoglaló, hogy mi micsoda: a drizzle-orm a fő ORM csomag, a @neondatabase/serverless a Neon PostgreSQL szerverless drivere (ha hagyományos PostgreSQL-t használsz, cseréld le a pg csomagra), a drizzle-kit pedig a migrációkezelő CLI eszköz.
A javasolt mappastruktúra a következő:
src/
├── app/
│ ├── page.tsx
│ └── actions/
│ └── tasks.ts # Server Actions
├── db/
│ ├── schema.ts # Drizzle séma
│ ├── index.ts # DB kapcsolat
│ └── migrations/ # Generált migrációk
├── lib/
│ └── validators.ts # Zod sémák
drizzle.config.ts # Drizzle Kit konfiguráció
.env.local # Környezeti változók
Természetesen ez nem kötelező — de tapasztalatom szerint ez a felépítés a legjobban skálázódik, ahogy nő a projekt.
Adatbázis-kapcsolat konfigurálása
A Drizzle ORM nem kezeli önmaga a kapcsolatokat — te hozod a saját driveredet. Ez elsőre talán kényelmetlennek tűnik, de valójában óriási előny a rugalmasság szempontjából. Könnyedén válthatsz Neon, Supabase, PlanetScale vagy hagyományos PostgreSQL között anélkül, hogy az ORM rétegen bármit változtatnál.
Hozd létre a src/db/index.ts fájlt:
Neon szerverless driver használata
// src/db/index.ts
import { neon } from "@neondatabase/serverless";
import { drizzle } from "drizzle-orm/neon-http";
import * as schema from "./schema";
const sql = neon(process.env.DATABASE_URL!);
export const db = drizzle(sql, { schema });
Hagyományos PostgreSQL (node-postgres) használata
// src/db/index.ts
import { drizzle } from "drizzle-orm/node-postgres";
import { Pool } from "pg";
import * as schema from "./schema";
const pool = new Pool({
connectionString: process.env.DATABASE_URL!,
max: 10,
ssl: process.env.NODE_ENV === "production"
? { rejectUnauthorized: false }
: false,
});
export const db = drizzle(pool, { schema });
Fontos: soha ne hozz létre adatbázis-kapcsolatot a modul betöltésekor szerverless környezetben közvetlen kapcsolattal. A Neon szerverless drivere HTTP-alapú, szóval ez ott nem probléma — de a pg Pool esetén figyelj arra, hogy a kapcsolat csak szükség esetén jöjjön létre.
Add hozzá a .env.local fájlhoz a kapcsolati sztringet:
DATABASE_URL=postgresql://user:password@host:5432/dbname?sslmode=require
Séma definiálás TypeScriptben
Na, ez az a pont, ahol a Drizzle ORM igazán megmutatja az erejét. A séma maga TypeScript — nincsen külön DSL, nincsen generálási lépés, nincsen semmi varázslat. A típusok automatikusan levezetődnek, és a séma egyben a migrációk meg a Zod validátorok forrása is.
Őszintén? Ez az, ami engem meggyőzött.
// src/db/schema.ts
import {
pgTable,
serial,
text,
varchar,
boolean,
timestamp,
integer,
} from "drizzle-orm/pg-core";
import { relations } from "drizzle-orm";
// Projektek tábla
export const projects = pgTable("projects", {
id: serial("id").primaryKey(),
name: varchar("name", { length: 255 }).notNull(),
description: text("description"),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.defaultNow()
.notNull()
.$onUpdate(() => new Date()),
});
// Feladatok tábla
export const tasks = pgTable("tasks", {
id: serial("id").primaryKey(),
title: varchar("title", { length: 255 }).notNull(),
description: text("description"),
completed: boolean("completed").default(false).notNull(),
priority: integer("priority").default(0).notNull(),
projectId: integer("project_id")
.references(() => projects.id, { onDelete: "cascade" })
.notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.defaultNow()
.notNull()
.$onUpdate(() => new Date()),
});
// Relációk definiálása
export const projectsRelations = relations(projects, ({ many }) => ({
tasks: many(tasks),
}));
export const tasksRelations = relations(tasks, ({ one }) => ({
project: one(projects, {
fields: [tasks.projectId],
references: [projects.id],
}),
}));
Egy fontos részlet, amit érdemes megjegyezni: a relációk külön vannak definiálva a tábla definícióktól. A Drizzle-ben a foreign key (adatbázis szintű) és a relation (ORM szintű) két különböző dolog. Mindkettőre szükséged van a teljes típusbiztonsághoz és a relációs lekérdezésekhez — ne hagyd ki egyiket sem.
Migrációk kezelése drizzle-kit-tel
A Drizzle Kit elemzi a sémádat és SQL migrációs fájlokat generál. Először hozd létre a konfigurációs fájlt:
// drizzle.config.ts
import { defineConfig } from "drizzle-kit";
export default defineConfig({
schema: "./src/db/schema.ts",
out: "./src/db/migrations",
dialect: "postgresql",
dbCredentials: {
url: process.env.DATABASE_URL!,
},
strict: true,
verbose: true,
});
A strict: true beállítás kritikus fontosságú — és ezt nem túlzásból mondom. Ez arra kényszeríti a drizzle-kit-et, hogy megerősítést kérjen, ha kétértelmű változást észlel. Például egy oszlop átnevezését egyébként törlés + hozzáadásként értelmezné, ami szép csendben adatvesztéshez vezetne. Nem vicces.
Most generáld le és futtasd a migrációkat:
# Migráció generálása a séma alapján
npx drizzle-kit generate
# Migráció alkalmazása az adatbázisra
npx drizzle-kit migrate
# Vagy fejlesztés közben: közvetlen push (migrációs fájl nélkül)
npx drizzle-kit push
A generate SQL fájlokat hoz létre a migrations mappában — ezeket mindig commitold a verziókezelőbe. A push parancs viszont fejlesztéshez remek, mivel közvetlenül alkalmazza a séma változtatásokat, migrációs fájl nélkül.
Adatlekérdezés Server Componentekben
Most jön az igazán szép rész. A Next.js App Router Server Componentjei tökéletesen illeszkednek a Drizzle ORM-hez: a komponensek a szerveren futnak, közvetlenül hozzáférnek az adatbázishoz, és a kliensre soha nem kerül ki sem a kapcsolat, sem az ORM kódja. Szóval nincs miért aggódni biztonsági szempontból.
Alapvető lekérdezés
// src/app/projects/page.tsx
import { db } from "@/db";
import { projects } from "@/db/schema";
import { desc } from "drizzle-orm";
export default async function ProjectsPage() {
const allProjects = await db
.select()
.from(projects)
.orderBy(desc(projects.createdAt));
return (
<main>
<h1>Projektek</h1>
<ul>
{allProjects.map((project) => (
<li key={project.id}>
<h2>{project.name}</h2>
<p>{project.description}</p>
</li>
))}
</ul>
</main>
);
}
Ugye milyen tiszta? Nincs API réteg, nincs fetch hívás — csak egy sima adatbázis lekérdezés, közvetlenül a komponensben.
Relációs lekérdezés (Drizzle Query API)
A Drizzle Query API lehetővé teszi a relációs lekérdezéseket SQL JOIN-ok nélkül. Ha használtad már a Prisma include szintaxisát, ez hasonló koncepció — csak SQL-közelibb megközelítéssel:
// src/app/projects/[id]/page.tsx
import { db } from "@/db";
import { eq } from "drizzle-orm";
interface Props {
params: Promise<{ id: string }>;
}
export default async function ProjectDetailPage({ params }: Props) {
const { id } = await params;
const project = await db.query.projects.findFirst({
where: (projects, { eq }) => eq(projects.id, parseInt(id)),
with: {
tasks: {
orderBy: (tasks, { desc }) => [desc(tasks.priority)],
},
},
});
if (!project) return <p>A projekt nem található.</p>;
return (
<main>
<h1>{project.name}</h1>
<p>{project.description}</p>
<h2>Feladatok ({project.tasks.length})</h2>
<ul>
{project.tasks.map((task) => (
<li key={task.id}>
<span>{task.completed ? "✓" : "○"}</span>
{task.title}
</li>
))}
</ul>
</main>
);
}
Egy apró de lényeges dolog: ahhoz, hogy a db.query API működjön, feltétlenül add át a séma objektumot a drizzle() hívásban. E nélkül a relációs lekérdezések egyszerűen nem érhetők el, és elég rejtélyes hibaüzeneteket fogsz kapni.
CRUD műveletek Server Actionökkel
A Server Actions a Next.js beépített megoldása a szerver oldali mutációkra. A Drizzle ORM-mel kombinálva tiszta, típusbiztos CRUD műveleteket hozhatsz létre — és ami a legjobb: API végpontok nélkül.
Nézzük végig az összes alapműveletet:
// src/app/actions/tasks.ts
"use server";
import { db } from "@/db";
import { tasks } from "@/db/schema";
import { eq } from "drizzle-orm";
import { revalidatePath } from "next/cache";
import { taskSchema } from "@/lib/validators";
// CREATE — Új feladat létrehozása
export async function createTask(formData: FormData) {
const raw = {
title: formData.get("title") as string,
description: formData.get("description") as string,
projectId: Number(formData.get("projectId")),
priority: Number(formData.get("priority") ?? 0),
};
const validated = taskSchema.parse(raw);
await db.insert(tasks).values(validated);
revalidatePath("/projects");
}
// READ — Egyedi feladat lekérdezése
export async function getTask(id: number) {
const task = await db.query.tasks.findFirst({
where: eq(tasks.id, id),
with: { project: true },
});
return task ?? null;
}
// UPDATE — Feladat frissítése
export async function updateTask(id: number, formData: FormData) {
const raw = {
title: formData.get("title") as string,
description: formData.get("description") as string,
priority: Number(formData.get("priority") ?? 0),
};
await db
.update(tasks)
.set({
title: raw.title,
description: raw.description,
priority: raw.priority,
})
.where(eq(tasks.id, id));
revalidatePath("/projects");
}
// DELETE — Feladat törlése
export async function deleteTask(id: number) {
await db.delete(tasks).where(eq(tasks.id, id));
revalidatePath("/projects");
}
// TOGGLE — Feladat állapot váltása
export async function toggleTask(id: number) {
const task = await db.query.tasks.findFirst({
where: eq(tasks.id, id),
});
if (!task) throw new Error("Feladat nem található");
await db
.update(tasks)
.set({ completed: !task.completed })
.where(eq(tasks.id, id));
revalidatePath("/projects");
}
Figyeld meg a revalidatePath használatát minden egyes mutáció után. Ez biztosítja, hogy a Next.js gyorsítótár frissüljön, és a felhasználó tényleg a legújabb adatokat lássa. Könnyű elfelejteni, de nélküle az UI elavult marad — ami frusztráló tud lenni.
Zod validáció integrálása
Szóval van egy Drizzle sémánk, amiből a TypeScript típusok automatikusan jönnek. De mi a helyzet a runtime validációval? Itt jön képbe a drizzle-zod csomag, ami automatikusan generál Zod sémákat a tábla definíciókból. Ezzel eliminálhatsz minden kézi szinkronizációt az adatbázis séma és a validációs szabályok között.
npm install drizzle-zod zod
// src/lib/validators.ts
import { createInsertSchema, createSelectSchema } from "drizzle-zod";
import { tasks } from "@/db/schema";
import { z } from "zod";
// Automatikusan generált insert séma
// a tábla definícióból
export const taskInsertSchema = createInsertSchema(tasks, {
title: z.string().min(1, "A cím megadása kötelező").max(255),
description: z.string().max(1000).optional(),
priority: z.number().int().min(0).max(5),
});
// Egyedi validációs séma űrlapokhoz
export const taskSchema = taskInsertSchema.pick({
title: true,
description: true,
projectId: true,
priority: true,
});
// Select séma a visszaadott adatokhoz
export const taskSelectSchema = createSelectSchema(tasks);
// Típus export
export type TaskInsert = z.infer<typeof taskInsertSchema>;
export type TaskSelect = z.infer<typeof taskSelectSchema>;
Az egész rendszer egyetlen forrásból táplálkozik (a Drizzle séma): az adatbázis struktúra, a TypeScript típusok és a validációs szabályok mind automatikusan szinkronban maradnak. Ha módosítod a sémát, minden más követi. Ennél szebb single source of truth-t nehéz elképzelni.
Összetett lekérdezések és tranzakciók
A valós alkalmazásokban persze ritkán van szükséged csak egyszerű CRUD-ra. Nézzük meg, hogyan kezelheted az összetettebb mintákat — mert itt mutatkozik meg igazán, hogy a Drizzle mennyire SQL-közelien gondolkodik.
Szűrés, lapozás és rendezés
import { db } from "@/db";
import { tasks } from "@/db/schema";
import { and, eq, ilike, desc, sql } from "drizzle-orm";
interface TaskFilters {
projectId: number;
search?: string;
completed?: boolean;
page?: number;
pageSize?: number;
}
export async function getFilteredTasks(filters: TaskFilters) {
const { projectId, search, completed, page = 1, pageSize = 20 } = filters;
const conditions = [eq(tasks.projectId, projectId)];
if (search) {
conditions.push(ilike(tasks.title, `%${search}%`));
}
if (completed !== undefined) {
conditions.push(eq(tasks.completed, completed));
}
const [data, countResult] = await Promise.all([
db
.select()
.from(tasks)
.where(and(...conditions))
.orderBy(desc(tasks.priority), desc(tasks.createdAt))
.limit(pageSize)
.offset((page - 1) * pageSize),
db
.select({ count: sql<number>`count(*)` })
.from(tasks)
.where(and(...conditions)),
]);
return {
tasks: data,
total: countResult[0].count,
totalPages: Math.ceil(countResult[0].count / pageSize),
};
}
Tranzakciók
Amikor több táblát érintő műveletet végzel, a tranzakciók biztosítják, hogy vagy minden sikerül, vagy semmi. Ez nem opcionális luxus — ez produkciós alapkövetelmény:
export async function createProjectWithTasks(
projectData: { name: string; description?: string },
taskList: { title: string; priority: number }[]
) {
return await db.transaction(async (tx) => {
const [newProject] = await tx
.insert(projects)
.values(projectData)
.returning();
if (taskList.length > 0) {
await tx.insert(tasks).values(
taskList.map((task) => ({
...task,
projectId: newProject.id,
completed: false,
}))
);
}
return newProject;
});
}
A tranzakción belül a tx objektumot használd a db helyett — ha bármelyik művelet hibát dob, az egész tranzakció automatikusan visszagörgetődik. Egyszerű és kiszámítható.
Connection pooling szerverless környezetben
A szerverless és edge környezetek speciális kihívást jelentenek az adatbázis-kapcsolatok kezelésében. Minden függvényhívás potenciálisan új kapcsolatot nyit, ami gyorsan kimerítheti az adatbázis kapcsolatlimitjét. (Volt már részem ehhez — nem szórakoztató éjjel 2-kor debugolni.)
Neon szerverless driver
A legegyszerűbb megoldás: a Neon szerverless drivere HTTP-alapú, tehát nincs szükség hagyományos connection poolingra. Minden lekérdezés egy önálló HTTP kérés — tökéletes szerverless környezethez:
// Neon HTTP driver — nincs pool probléma
import { neon } from "@neondatabase/serverless";
import { drizzle } from "drizzle-orm/neon-http";
const sql = neon(process.env.DATABASE_URL!);
export const db = drizzle(sql, { schema });
Hagyományos PostgreSQL connection poolinggal
Ha saját PostgreSQL szervert vagy nem HTTP-alapú providert használsz, szükséged lesz connection poolingra. A leggyakoribb megoldás a PgBouncer vagy a Supabase poolere:
// Connection pooling URL-lel (pl. Supabase)
import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
// A pooler URL-t használd, nem a közvetlen kapcsolatot
const client = postgres(process.env.DATABASE_POOLER_URL!, {
prepare: false, // Szükséges tranzakció pooling módban
});
export const db = drizzle(client, { schema });
A prepare: false beállítás kritikus, ha PgBouncer tranzakció módban fut. A prepared statementek nem működnek ilyen konfigurációban, és a hibaüzenet sajnos nem mindig egyértelmű — szóval jobb, ha előre beállítod.
Edge Runtime kompatibilitás
A Drizzle ORM mindössze ~7,4 KB gzippelve. Összehasonlításképpen a Prisma Client ~2 MB. Ez a különbség a cold start idők szempontjából döntő a Vercel Edge Runtime vagy Cloudflare Workers környezetben. A Drizzle natívan edge-kompatibilis, nincs szükség kiegészítő szolgáltatásra (mint a Prisma Accelerate).
Produkciós best practice-ek
Összegyűjtöttem azokat a mintákat, amelyeket a valós, produkciós Next.js + Drizzle projektek során a legfontosabbnak tartok. Ezek azok a dolgok, amikről ritkán beszélnek a tutorialok, de élesben nélkülözhetetlenek.
1. Soha ne használj sql.raw()-t felhasználói inputtal
A Drizzle sql template literal automatikusan paraméterezi a lekérdezéseket — de a sql.raw() nem. Soha, de tényleg soha ne adj felhasználói inputot a sql.raw()-nak, mert SQL injection támadásnak nyitod meg az ajtót.
// HELYTELEN — SQL injection veszély!
const result = await db.execute(
sql.raw(`SELECT * FROM tasks WHERE title = '${userInput}'`)
);
// HELYES — paraméterezett lekérdezés
const result = await db.execute(
sql`SELECT * FROM tasks WHERE title = ${userInput}`
);
2. Használj Data Access Layer-t
Ne szórd szét a db hívásokat a komponenseidben — hosszú távon káosz lesz belőle. Inkább hozz létre egy dedikált adatelérési réteget:
// src/db/queries/tasks.ts
import { db } from "@/db";
import { tasks } from "@/db/schema";
import { eq, and, sql } from "drizzle-orm";
export const taskQueries = {
getAll: (projectId: number) =>
db.query.tasks.findMany({
where: eq(tasks.projectId, projectId),
with: { project: true },
}),
getById: (id: number) =>
db.query.tasks.findFirst({
where: eq(tasks.id, id),
}),
getCompletedCount: (projectId: number) =>
db
.select({ count: sql<number>`count(*)` })
.from(tasks)
.where(
and(
eq(tasks.projectId, projectId),
eq(tasks.completed, true)
)
),
};
3. Indexek a sémában
A Drizzle lehetővé teszi indexek definiálását közvetlenül a sémában — és érdemes is élni vele, mert így a migrációk automatikusan tartalmazzák őket:
import { pgTable, serial, varchar, integer, index } from "drizzle-orm/pg-core";
export const tasks = pgTable(
"tasks",
{
id: serial("id").primaryKey(),
title: varchar("title", { length: 255 }).notNull(),
projectId: integer("project_id").notNull(),
priority: integer("priority").default(0).notNull(),
},
(table) => [
index("tasks_project_id_idx").on(table.projectId),
index("tasks_priority_idx").on(table.priority),
]
);
4. Prepared statementek ismétlődő lekérdezésekhez
import { db } from "@/db";
import { tasks } from "@/db/schema";
import { eq, sql } from "drizzle-orm";
// Egyszer definiálva, többször felhasználva
const getTasksByProject = db
.select()
.from(tasks)
.where(eq(tasks.projectId, sql.placeholder("projectId")))
.prepare("get_tasks_by_project");
// Használat
const result = await getTasksByProject.execute({ projectId: 42 });
A prepared statementek kihagyják a lekérdezés-tervezési lépést, ami mérhető teljesítményjavulást jelent gyakran futtatott lekérdezéseknél. Nem hatalmas különbség egyenként, de összeadódik.
5. Migrációk commitolása és strict mód
Mindig commitold a generált migrációs fájlokat a verziókezelőbe, és mindig használd a strict: true beállítást a drizzle.config.ts-ben. Komolyan. A strict mód nélkül a drizzle-kit egy oszlop átnevezését törlés + hozzáadásként értelmezheti, ami adatvesztéshez vezet. Ezt a hibát elég egyszer elkövetni.
Összefoglalás
A Drizzle ORM és a Next.js App Router kombinációja 2026-ban szerintem a leghatékonyabb módja a típusbiztos, teljes stackes alkalmazások fejlesztésének. A séma TypeScriptben van, a típusok automatikusan szinkronban maradnak, a Server Components közvetlenül lekérdezik az adatbázist, a Server Actions pedig biztonságos mutációkat biztosítanak — mindezt egyetlen, koherens adatforrásból.
A Drizzle könnyűsúlyú jellege, edge-kompatibilitása és SQL-közeli szintaxisa ideálissá teszi a modern szerverless környezetekhez. Ha még nem próbáltad, ideje belevágni — a tanulási görbe meglepően rövid, különösen ha van SQL tapasztalatod.
Gyakran Ismételt Kérdések
Mikor válasszam a Drizzle ORM-et a Prisma helyett Next.js-hez?
A Drizzle-t akkor érdemes választani, ha edge vagy szerverless környezetbe deployolsz (a bundle méret lényegesen kisebb: ~7,4 KB vs ~2 MB), ha SQL-közeli szintaxist preferálsz, vagy ha nem akarsz külön kódgenerálási lépést. A Prisma viszont erősebb választás, ha a csapatod még nem jártas az SQL-ben, vagy ha szükséged van a Prisma Accelerate globális connection poolingjára.
Hogyan kezeljem a connection poolingot Drizzle ORM-mel Vercel szerverless környezetben?
A legegyszerűbb megoldás a Neon szerverless driver használata — az HTTP-alapú, szóval nincs szüksége hagyományos poolingra. Ha hagyományos PostgreSQL-t használsz, állíts be PgBouncer-t vagy használd a Supabase beépített poolerét, és ne feledkezz meg a prepare: false beállításról tranzakció pooling módban.
Biztonságos a Drizzle ORM az SQL injection ellen?
Igen. A Drizzle ORM sql template literálja automatikusan paraméterezi a lekérdezéseket, ami védelmet nyújt SQL injection ellen. Az egyetlen kivétel a sql.raw(), amelyet soha ne használj felhasználói inputtal. A query builder és az eq(), ilike() és hasonló operátorok szintén biztonságosak.
Működik a Drizzle ORM a Next.js Edge Runtime-mal?
Igen, natívan. Nincs benne natív bináris (mint a Prisma korábbi verzióiban), mindössze ~7,4 KB gzippelve, és működik Vercel Edge Runtime, Cloudflare Workers és minden más edge környezetben. A Neon vagy PlanetScale szerverless driverekkel kombinálva tökéletes edge-first adatbázis réteget kapsz.
Hogyan tartom szinkronban a Drizzle sémát, a TypeScript típusokat és a Zod validációt?
A Drizzle séma egyben a TypeScript típusok forrása is — nincs szükség külön generate lépésre. A drizzle-zod csomaggal a createInsertSchema() és createSelectSchema() függvényekkel automatikusan generálhatsz Zod validátorokat a tábla definíciókból. Egyetlen forrásból származik minden: az adatbázis struktúra, a típusok és a validáció.