Защо Drizzle ORM е правилният избор за Next.js през 2026 г.
Ако вече сте се борили със Server Actions и кеширане в Next.js 15, вероятно сте стигнали до онзи момент, в който ви трябва истинска база данни. И тук идва въпросът — кой ORM да изберете?
Честно казано, през 2026 г. отговорът все по-често е Drizzle ORM.
Drizzle е TypeScript-базиран ORM с една проста философия: ако знаете SQL, вече знаете Drizzle. За разлика от Prisma, който абстрахира SQL зад собствен query език и изисква стъпка за генериране на код, Drizzle работи директно с TypeScript. Схемата ви е TypeScript, заявките изглеждат като SQL, а типовете се извеждат автоматично — без допълнителни стъпки.
Ето защо Drizzle се вписва толкова добре в Next.js 15:
- Нулева генерация на код — Няма
prisma generate. Типовете се актуализират веднага при редактиране на схемата, което работи безпроблемно с Turbopack и Fast Refresh. - Минимален размер на бъндъла — Drizzle е само ~7.4 KB, което е критично за serverless функции и Edge Runtime.
- Нативна поддръжка на Edge — Работи директно на Vercel Edge Functions и Cloudflare Workers без допълнителни proxy услуги.
- SQL-ориентиран подход — Заявките са предсказуеми и прозрачни, защото следват SQL синтаксиса.
В това ръководство ще изградим пълен full-stack проект с Next.js 15 и Drizzle ORM — от настройката и миграциите до извличане на данни в Server Components и мутации чрез Server Actions. Ако сте чели предишните ни статии за Server Actions и кеширане, това е логичното продължение.
Настройка на проекта
Инсталация на зависимостите
Започнете с нов или съществуващ Next.js 15 проект и инсталирайте необходимите пакети:
# Основни пакети
npm install drizzle-orm pg
npm install -D drizzle-kit @types/pg
# За Zod валидация (по избор, но препоръчително)
npm install drizzle-zod zod
Пакетът drizzle-orm е самият ORM, pg е PostgreSQL драйверът за Node.js, а drizzle-kit е CLI инструментът за миграции. Ако ползвате Neon Serverless Postgres, заменете pg с @neondatabase/serverless.
Структура на проекта
Аз лично препоръчвам следната структура — поддържа нещата чисти и при мащабиране не се превръща в хаос:
app/
(dashboard)/
projects/
page.tsx # Server Component — извлича данни
new/
page.tsx # Формуляр за нов проект
actions/
project.ts # Server Actions за CRUD
src/
db/
index.ts # Връзка с базата данни (singleton)
schema.ts # Drizzle схема (таблици + релации)
migrations/ # Генерирани SQL миграции
drizzle.config.ts # Конфигурация на drizzle-kit
.env.local # DATABASE_URL
Конфигуриране на връзката с базата данни
Създайте файл src/db/index.ts със singleton шаблон. Това е критично важно — без singleton, всяко hot reload по време на разработка създава нова връзка, докато connection pool-ът се изчерпи. Повярвайте ми, дебъгването на този проблем не е забавно.
// 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!,
})
// Singleton шаблон за development
const globalForDb = globalThis as unknown as {
db: ReturnType<typeof drizzle> | undefined
}
export const db = globalForDb.db ?? drizzle(pool, { schema })
if (process.env.NODE_ENV !== 'production') {
globalForDb.db = db
}
Забележете, че подаваме schema като опция на drizzle(). Това активира релационните заявки (Relational Queries API), за които ще говорим по-нататък.
Вариант за Neon Serverless
Ако ползвате Neon (което е доста популярен избор за Vercel), връзката изглежда малко по-различно:
// src/db/index.ts (за Neon Serverless)
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 })
Дефиниране на схемата
Таблици и колони
Drizzle използва TypeScript функции за дефиниране на таблиците. Ако знаете SQL, ще ви се стори доста познато:
// src/db/schema.ts
import {
pgTable,
serial,
text,
varchar,
timestamp,
integer,
boolean,
pgEnum
} from 'drizzle-orm/pg-core'
// Enum тип
export const projectStatusEnum = pgEnum('project_status', [
'active',
'completed',
'archived'
])
// Таблица за потребители
export const users = pgTable('users', {
id: serial('id').primaryKey(),
name: varchar('name', { length: 255 }).notNull(),
email: varchar('email', { length: 255 }).unique().notNull(),
avatarUrl: text('avatar_url'),
createdAt: timestamp('created_at').defaultNow().notNull(),
})
// Таблица за проекти
export const projects = pgTable('projects', {
id: serial('id').primaryKey(),
title: varchar('title', { length: 255 }).notNull(),
description: text('description'),
status: projectStatusEnum('status').default('active').notNull(),
ownerId: integer('owner_id').references(() => users.id).notNull(),
isPublic: boolean('is_public').default(false).notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
})
// Таблица за задачи
export const tasks = pgTable('tasks', {
id: serial('id').primaryKey(),
title: varchar('title', { length: 255 }).notNull(),
completed: boolean('completed').default(false).notNull(),
projectId: integer('project_id').references(() => projects.id, {
onDelete: 'cascade'
}).notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
})
Всеки аргумент на pgTable директно отговаря на SQL — serial е SERIAL, varchar е VARCHAR, references е FOREIGN KEY. Няма магия, няма скрити абстракции. Точно това ми харесва в Drizzle.
Дефиниране на релации
Релациите в Drizzle са отделни от таблиците. Те не създават SQL constraints — вместо това се използват от Relational Queries API за автоматични JOIN-ове:
// src/db/schema.ts (продължение)
import { relations } from 'drizzle-orm'
export const usersRelations = relations(users, ({ many }) => ({
projects: many(projects),
}))
export const projectsRelations = relations(projects, ({ one, many }) => ({
owner: one(users, {
fields: [projects.ownerId],
references: [users.id],
}),
tasks: many(tasks),
}))
export const tasksRelations = relations(tasks, ({ one }) => ({
project: one(projects, {
fields: [tasks.projectId],
references: [projects.id],
}),
}))
Извеждане на TypeScript типове
Едно от най-готините предимства на Drizzle — типовете се извеждат директно от схемата, без никаква генерация на код:
// src/db/schema.ts (продължение)
import type { InferSelectModel, InferInsertModel } from 'drizzle-orm'
// Типове за SELECT (четене)
export type User = InferSelectModel<typeof users>
export type Project = InferSelectModel<typeof projects>
export type Task = InferSelectModel<typeof tasks>
// Типове за INSERT (писане)
export type NewProject = InferInsertModel<typeof projects>
export type NewTask = InferInsertModel<typeof tasks>
Тези типове се актуализират моментално при промяна на схемата. Няма нужда да пускате генератор. Просто работи.
Миграции с drizzle-kit
Конфигуриране на drizzle-kit
Създайте файл drizzle.config.ts в корена на проекта:
// drizzle.config.ts
import { defineConfig } from 'drizzle-kit'
export default defineConfig({
schema: './src/db/schema.ts',
out: './src/db/migrations',
dialect: 'postgresql',
strict: true,
verbose: true,
dbCredentials: {
url: process.env.DATABASE_URL!,
},
})
Опцията strict: true е задължителна за production. Без нея Drizzle може да реши, че преименуване на колона означава „изтрий старата и създай нова", което води до загуба на данни. Не е забавно.
Генериране и прилагане на миграции
Добавете тези скриптове в package.json:
{
"scripts": {
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate",
"db:push": "drizzle-kit push",
"db:studio": "drizzle-kit studio"
}
}
Работният процес изглежда така:
- По време на разработка — използвайте
npm run db:pushза бърза синхронизация на схемата без миграционни файлове. - Преди production — генерирайте миграция с
npm run db:generate, прегледайте генерирания SQL, и приложете сnpm run db:migrate. - За визуален преглед — стартирайте
npm run db:studioза Drizzle Studio, уеб интерфейс за управление на данните.
# Генериране на миграция
npm run db:generate
# Преглед на генерирания SQL
cat src/db/migrations/0001_smooth_initial.sql
# Прилагане на миграцията
npm run db:migrate
Извличане на данни в Server Components
Тук е едно от наистина елегантните неща в комбинацията Next.js 15 + Drizzle. В Server Components можете да извличате данни директно — без API маршрути, без useEffect, без loading state. Нека видим как.
Базови заявки
// app/(dashboard)/projects/page.tsx
import { db } from '@/db'
import { projects, users } from '@/db/schema'
import { eq, desc } from 'drizzle-orm'
export default async function ProjectsPage() {
// SELECT * FROM projects ORDER BY created_at DESC
const allProjects = await db
.select()
.from(projects)
.orderBy(desc(projects.createdAt))
return (
<div>
<h1>Проекти</h1>
{allProjects.map(project => (
<div key={project.id}>
<h2>{project.title}</h2>
<p>Статус: {project.status}</p>
</div>
))}
</div>
)
}
JOIN заявки
Drizzle поддържа два начина за JOIN заявки — SQL-стил и Relational Queries API. И двата работят добре, но имат различни случаи на употреба:
// SQL-стил JOIN
const projectsWithOwners = await db
.select({
id: projects.id,
title: projects.title,
ownerName: users.name,
ownerEmail: users.email,
})
.from(projects)
.innerJoin(users, eq(projects.ownerId, users.id))
.where(eq(projects.isPublic, true))
// Relational Queries API — по-удобен за вложени обекти
const projectsWithTasks = await db.query.projects.findMany({
where: eq(projects.status, 'active'),
with: {
owner: true,
tasks: {
where: eq(tasks.completed, false),
orderBy: desc(tasks.createdAt),
},
},
})
// Резултатът е автоматично типизиран:
// { id: number, title: string, owner: User, tasks: Task[] }[]
Relational Queries API е за предпочитане, когато имате нужда от вложени обекти. Той генерира оптимизирани SQL заявки с подзаявки вместо множество JOIN-ове, което при сложни релации се усеща.
Интеграция с кеширане
Ако сте чели статията ни за кеширане в Next.js, знаете за use cache директивата. Ето как се комбинира с Drizzle:
// lib/queries/projects.ts
import 'use cache'
import { cacheLife, cacheTag } from 'next/cache'
import { db } from '@/db'
import { projects } from '@/db/schema'
import { eq } from 'drizzle-orm'
export async function getProjectById(id: number) {
cacheLife('hours')
cacheTag(`project-${id}`)
return db.query.projects.findFirst({
where: eq(projects.id, id),
with: { tasks: true, owner: true },
})
}
CRUD операции със Server Actions
Сега идва най-интересната част — мутациите. Ако сте чели ръководството ни за Server Actions, знаете основите. Тук ще ги комбинираме с Drizzle за пълен CRUD и (спойлер) работи страхотно.
Валидация с drizzle-zod
Пакетът drizzle-zod автоматично генерира Zod схеми от Drizzle таблиците. Това означава, че валидацията е винаги синхронизирана с базата данни — промените в схемата се отразяват навсякъде:
// lib/validations/project.ts
import { createInsertSchema, createSelectSchema } from 'drizzle-zod'
import { projects } from '@/db/schema'
import { z } from 'zod'
// Автоматично генерирана схема от таблицата
export const insertProjectSchema = createInsertSchema(projects, {
// Допълнителни правила над автоматичните
title: z.string().min(3, 'Заглавието трябва да е поне 3 символа').max(255),
description: z.string().max(2000).optional(),
})
// Само полетата, които потребителят попълва
export const createProjectFormSchema = insertProjectSchema.pick({
title: true,
description: true,
isPublic: true,
})
export type CreateProjectInput = z.infer<typeof createProjectFormSchema>
Създаване (Create)
// app/actions/project.ts
'use server'
import { db } from '@/db'
import { projects } from '@/db/schema'
import { createProjectFormSchema } from '@/lib/validations/project'
import { revalidateTag } from 'next/cache'
import { redirect } from 'next/navigation'
export type ActionState = {
errors?: Record<string, string[]>
message?: string
success?: boolean
}
export async function createProject(
prevState: ActionState,
formData: FormData
): Promise<ActionState> {
// Валидация с drizzle-zod + Zod
const parsed = createProjectFormSchema.safeParse({
title: formData.get('title'),
description: formData.get('description'),
isPublic: formData.get('isPublic') === 'on',
})
if (!parsed.success) {
return {
errors: parsed.error.flatten().fieldErrors as Record<string, string[]>,
}
}
try {
const [newProject] = await db
.insert(projects)
.values({
...parsed.data,
ownerId: 1, // В реално приложение — от сесията
})
.returning()
revalidateTag('projects')
redirect(`/projects/${newProject.id}`)
} catch (error) {
return {
message: 'Грешка при създаване на проекта. Опитайте отново.',
}
}
}
Четене (Read)
// app/actions/project.ts (продължение)
import { eq, like, and, desc } from 'drizzle-orm'
export async function getProjects(search?: string) {
const conditions = []
if (search) {
conditions.push(like(projects.title, `%${search}%`))
}
return db.query.projects.findMany({
where: conditions.length ? and(...conditions) : undefined,
with: {
owner: { columns: { name: true, avatarUrl: true } },
tasks: true,
},
orderBy: desc(projects.createdAt),
})
}
Обновяване (Update)
// app/actions/project.ts (продължение)
export async function updateProject(
prevState: ActionState,
formData: FormData
): Promise<ActionState> {
const id = Number(formData.get('id'))
const parsed = createProjectFormSchema.safeParse({
title: formData.get('title'),
description: formData.get('description'),
isPublic: formData.get('isPublic') === 'on',
})
if (!parsed.success) {
return {
errors: parsed.error.flatten().fieldErrors as Record<string, string[]>,
}
}
try {
await db
.update(projects)
.set({
...parsed.data,
updatedAt: new Date(),
})
.where(eq(projects.id, id))
revalidateTag(`project-${id}`)
revalidateTag('projects')
return { success: true, message: 'Проектът е обновен успешно.' }
} catch (error) {
return { message: 'Грешка при обновяването. Опитайте отново.' }
}
}
Изтриване (Delete)
// app/actions/project.ts (продължение)
export async function deleteProject(id: number): Promise<ActionState> {
try {
await db
.delete(projects)
.where(eq(projects.id, id))
revalidateTag('projects')
redirect('/projects')
} catch (error) {
return { message: 'Грешка при изтриването. Опитайте отново.' }
}
}
Клиентски компонент за формуляра
Ето как изглежда клиентският компонент, който ползва useActionState от React 19:
// app/(dashboard)/projects/new/create-project-form.tsx
'use client'
import { useActionState } from 'react'
import { createProject, type ActionState } from '@/app/actions/project'
const initialState: ActionState = {}
export function CreateProjectForm() {
const [state, formAction, isPending] = useActionState(
createProject,
initialState
)
return (
<form action={formAction} className="space-y-4 max-w-md">
{state.message && (
<div className="bg-red-50 border border-red-200 p-3 rounded">
<p className="text-red-600">{state.message}</p>
</div>
)}
<div>
<label htmlFor="title">Заглавие</label>
<input
id="title"
name="title"
type="text"
required
className="w-full border rounded p-2"
/>
{state.errors?.title && (
<p className="text-red-500 text-sm">{state.errors.title[0]}</p>
)}
</div>
<div>
<label htmlFor="description">Описание</label>
<textarea
id="description"
name="description"
rows={4}
className="w-full border rounded p-2"
/>
</div>
<div className="flex items-center gap-2">
<input id="isPublic" name="isPublic" type="checkbox" />
<label htmlFor="isPublic">Публичен проект</label>
</div>
<button
type="submit"
disabled={isPending}
className="bg-blue-600 text-white px-4 py-2 rounded disabled:opacity-50"
>
{isPending ? 'Създаване...' : 'Създай проект'}
</button>
</form>
)
}
Транзакции и напреднали операции
Транзакции
За операции, които трябва да се изпълнят атомарно (или всичко минава, или нищо), Drizzle има удобен API:
export async function createProjectWithTasks(
projectData: NewProject,
taskTitles: string[]
) {
return db.transaction(async (tx) => {
// Създаване на проекта
const [project] = await tx
.insert(projects)
.values(projectData)
.returning()
// Създаване на задачите
if (taskTitles.length > 0) {
await tx.insert(tasks).values(
taskTitles.map(title => ({
title,
projectId: project.id,
}))
)
}
return project
})
}
Ако някоя от операциите хвърли грешка, цялата транзакция се откатва автоматично. Просто и ефективно.
Подготвени заявки (Prepared Statements)
За заявки, които се изпълняват многократно, подготвените заявки подобряват производителността — планът на заявката се компилира веднъж и се преизползва:
import { sql } from 'drizzle-orm'
const getProjectBySlug = db
.select()
.from(projects)
.where(eq(projects.title, sql.placeholder('title')))
.prepare('get_project_by_slug')
// Използване
const result = await getProjectBySlug.execute({ title: 'My Project' })
Drizzle ORM срещу Prisma: Кога какво да изберете
Тъй като Prisma все още е популярен, нека направим кратко сравнение за 2026 г.:
- Производителност — Drizzle е в рамките на 10-20% от суровата SQL производителност. Prisma 7 значително подобри скоростта си, но все пак добавя повече overhead.
- Размер на бъндъла — Drizzle е ~7.4 KB. Prisma Client е значително по-голям. На serverless среди като Vercel Functions или AWS Lambda разликата в cold start е осезаема.
- Edge Runtime — Drizzle работи нативно на Edge. Prisma изисква Prisma Accelerate (платена proxy услуга).
- Опит за разработчици — Prisma е по-лесен за начинаещи, които не са комфортни със SQL. Drizzle изисква познаване на SQL, но дава пълен контрол.
- Миграции — И двата имат зрели миграционни системи. Drizzle генерира по-чист и предсказуем SQL.
Моето правило на палеца: Изберете Prisma, ако искате ORM, който мисли вместо вас. Изберете Drizzle, ако искате ORM, който мисли заедно с вас.
Deployment: Vercel + Neon
Препоръчителният стек за production deployment през 2026 г. е Vercel + Neon Serverless Postgres. Настройката е сравнително проста:
- Създайте Neon проект и копирайте connection string-а.
- Добавете
DATABASE_URLв Vercel Environment Variables. - Добавете миграционен скрипт в build процеса:
{
"scripts": {
"build": "npm run db:migrate && next build"
}
}
Neon предлага безплатен план с 0.5 GB storage, който е достатъчен за повечето странични проекти и прототипи.
Ако Neon не ви допада, ето алтернативи, които работят добре с Drizzle:
- Supabase — managed PostgreSQL с допълнителни функции като real-time и auth.
- PlanetScale — serverless MySQL с branching модел.
- Turso — SQLite на edge, идеален за ултра-ниска латентност.
- Vercel Postgres — интегрирано решение (базирано на Neon).
Най-добри практики за 2026 г.
- Винаги използвайте singleton шаблон в development — без него ще изчерпите connection pool-а при всяко hot reload.
- Активирайте strict режим в
drizzle.config.ts— предотвратява случайна загуба на данни при преименуване на колони. - Използвайте drizzle-zod за валидация — гарантира, че валидационните схеми са винаги синхронизирани с базата данни.
- Организирайте заявките в отделни файлове — създайте
lib/queries/директория за повторно използваеми заявки. - Не правете database заявки в клиентски компоненти — Drizzle трябва да се използва само в Server Components, Server Actions и API Route Handlers.
- Комбинирайте с кеширане — използвайте
cacheTagиrevalidateTagза интелигентно кеширане и ревалидация. - Прегледайте миграциите преди production — винаги четете генерираните SQL файлове. Отделете 2 минути да проверите, вместо да дебъгвате 2 часа.
Често задавани въпроси
Мога ли да използвам Drizzle ORM с SQLite в Next.js?
Да, без проблем. Drizzle поддържа SQLite чрез better-sqlite3 за локална разработка или @libsql/client за Turso (SQLite на edge). Просто ползвайте drizzle-orm/better-sqlite3 вместо drizzle-orm/node-postgres и sqliteTable вместо pgTable за дефиниране на схемата.
Как се справя Drizzle с connection pooling при serverless deployment?
При serverless deployment на Vercel всяко извикване на функция може да създаде нова връзка. За PostgreSQL препоръчителното решение е Neon Serverless Driver (@neondatabase/serverless), който използва HTTP вместо постоянни TCP връзки. Алтернативно, можете да конфигурирате PgBouncer или подобен connection pooler между приложението и базата данни.
Drizzle ORM безплатен ли е за production употреба?
Да, напълно безплатен и с отворен код (Apache-2.0 лиценз). Drizzle Studio също е безплатен за локална употреба. Единственият евентуален разход е за самата database услуга (Neon, Supabase и др.), но повечето предлагат щедри безплатни планове.
Как да направя миграция от Prisma към Drizzle ORM?
Можете да мигрирате поетапно, без да правите всичко наведнъж. Drizzle може да работи с вече съществуваща PostgreSQL база данни — използвайте drizzle-kit introspect (бивш pull), за да генерирате Drizzle схема от текущата структура. След това постепенно заменяйте Prisma заявките с Drizzle. Двата ORM-а могат спокойно да съществуват заедно в един проект по време на прехода.
Поддържа ли Drizzle ORM релационни заявки като Prisma includes?
Да, чрез Relational Queries API. Дефинирайте релации с relations() функцията и подайте схемата при инициализация на Drizzle. След това можете да ползвате db.query.tableName.findMany({ with: { relation: true } }) за зареждане на свързани данни — подобно на Prisma include, но с оптимизирани подзаявки вместо множество JOIN-ове.