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 през 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-ове.

За Автора Editorial Team

Our team of expert writers and editors.