Drizzle ORM в Next.js 15: Пълно ръководство за база данни със Server Components

Научете как да интегрирате Drizzle ORM с Next.js 15 — от настройка на PostgreSQL и миграции с drizzle-kit до типово-безопасни CRUD операции със Server Components, Server Actions и drizzle-zod валидация.

Drizzle ORM в Next.js 15: Пълен Setup 2026

Защо 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"
  }
}

Работният процес изглежда така:

  1. По време на разработка — използвайте npm run db:push за бърза синхронизация на схемата без миграционни файлове.
  2. Преди production — генерирайте миграция с npm run db:generate, прегледайте генерирания SQL, и приложете с npm run db:migrate.
  3. За визуален преглед — стартирайте 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. Настройката е сравнително проста:

  1. Създайте Neon проект и копирайте connection string-а.
  2. Добавете DATABASE_URL в Vercel Environment Variables.
  3. Добавете миграционен скрипт в 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 г.

  1. Винаги използвайте singleton шаблон в development — без него ще изчерпите connection pool-а при всяко hot reload.
  2. Активирайте strict режим в drizzle.config.ts — предотвратява случайна загуба на данни при преименуване на колони.
  3. Използвайте drizzle-zod за валидация — гарантира, че валидационните схеми са винаги синхронизирани с базата данни.
  4. Организирайте заявките в отделни файлове — създайте lib/queries/ директория за повторно използваеми заявки.
  5. Не правете database заявки в клиентски компоненти — Drizzle трябва да се използва само в Server Components, Server Actions и API Route Handlers.
  6. Комбинирайте с кеширане — използвайте cacheTag и revalidateTag за интелигентно кеширане и ревалидация.
  7. Прегледайте миграциите преди 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-ове.

За Автора Priya Raghavan

Priya spent four years on Vercel's developer experience team, where she wrote a chunk of the Turbopack incremental cache layer and rewrote the official App Router migration codemods used by tens of thousands of teams. Before that, she shipped the checkout funnel rewrite at Shopify on Next.js 12, cutting LCP from 3.4s to 1.1s on mid-tier Android. These days she runs an independent consultancy helping Series B companies untangle Pages Router monoliths into RSC-first App Router architectures. She has opinions about Server Actions, mostly negative ones, and is slowly coming around on Partial Prerendering after benchmarking it against six production workloads. She writes about cache semantics, streaming SSR, and the parts of the React docs that quietly contradict the Next.js docs. Eight years in frontend, three of them spent yelling at hydration mismatches.