Drizzle ORM în Next.js 16: Ghid Complet CRUD cu Server Components și Server Actions

Învață cum să construiești o aplicație CRUD completă cu Next.js 16 și Drizzle ORM. Ghid pas cu pas cu Server Components, Server Actions, validare Zod și PostgreSQL via Neon.

Dacă ai urmărit seria noastră despre Next.js 16, probabil deja știi cum merg Server Actions cu validare Zod, cum gestionezi cache-ul cu use cache și cum ai migrat de la middleware la proxy.ts. Ei bine, acum vine partea care leagă totul într-un mod concret: conectarea la o bază de date reală. Și sincer, pentru asta Drizzle ORM e cea mai bună alegere în 2026.

Drizzle ORM a depășit Prisma ca ORM preferat al comunității Next.js — cu peste 25.000 de stele pe GitHub, performanță apropiată de SQL brut și un footprint de doar ~33KB. Compară asta cu cele ~800KB ale motorului Prisma și diferența devine greu de ignorat, mai ales pe medii serverless și edge.

În acest ghid, vom construi o aplicație CRUD funcțională de la zero, cu Next.js 16, Drizzle ORM, PostgreSQL (via Neon) și Server Actions. O să vezi cum definești schema, cum creezi operațiuni de creare, citire, actualizare și ștergere — totul type-safe și validat cu Zod.

De ce Drizzle ORM în loc de Prisma?

Înainte să scriem cod, merită să înțelegi de ce Drizzle a câștigat atât de mult teren în ultimii doi ani.

Nu e vorba doar de performanță — deși, recunosc, asta contează enorm. Drizzle adoptă o filozofie complet diferită: schema ta este TypeScript pur. Nu există fișiere .prisma separate, nu există un pas de generare (prisma generate) care trebuie rulat după fiecare modificare. Tipurile se actualizează instant pe măsură ce editezi schema, ceea ce face experiența de dezvoltare mult mai fluidă. Sincer, după ce te obișnuiești cu asta, e greu să te mai întorci la Prisma.

Iată o comparație concretă:

  • Performanță: Drizzle rulează la 10-20% de SQL brut. Prisma adaugă un overhead de 2-4x datorită motorului de query.
  • Dimensiune bundle: Drizzle ~33KB vs. Prisma ~800KB — crucial pentru cold starts pe serverless.
  • Edge Runtime: Drizzle funcționează nativ pe Vercel Edge, Cloudflare Workers, Deno. Prisma nu prea e recomandat pentru Edge Functions.
  • Type safety: Ambele oferă type safety excelent, dar Drizzle o face fără un pas de generare intermediar.
  • Sintaxă SQL: Drizzle folosește o sintaxă apropiată de SQL, ceea ce îl face familiar pentru oricine cunoaște deja SQL.

Pentru proiecte Next.js 16 noi, în 2026, Drizzle e alegerea mai ușoară — mai ales dacă deployezi pe Vercel sau orice platformă serverless.

Configurarea proiectului

Cerințe preliminare

Pentru acest tutorial ai nevoie de:

  • Node.js 20+ instalat
  • Un proiect Next.js 16 (sau poți crea unul nou, desigur)
  • Un cont Neon (bază de date PostgreSQL serverless gratuită) sau orice instanță PostgreSQL

Crearea proiectului Next.js 16

Dacă pornești de la zero:

npx create-next-app@latest my-crud-app
cd my-crud-app

Selectează App Router, TypeScript și Tailwind CSS când ți se cere. Next.js 16 folosește Turbopack implicit, deci nu mai trebuie să configurezi nimic suplimentar pentru bundler — un lucru mai puțin de care să-ți faci griji.

Instalarea dependențelor Drizzle

Instalează Drizzle ORM împreună cu driver-ul pentru Neon și utilitarele necesare:

# Drizzle ORM si driver-ul Neon serverless
npm install drizzle-orm @neondatabase/serverless

# Drizzle Kit (pentru migrări) si Zod (pentru validare)
npm install -D drizzle-kit

# Zod pentru validare
npm install zod

Dacă folosești PostgreSQL local în loc de Neon, instalează driver-ul postgres în loc de @neondatabase/serverless:

npm install drizzle-orm postgres
npm install -D drizzle-kit

Configurarea variabilelor de mediu

Creează un fișier .env.local în rădăcina proiectului:

DATABASE_URL=postgresql://user:[email protected]/mydb?sslmode=require

Dacă folosești Neon, connection string-ul îl găsești în dashboard-ul Neon, secțiunea „Connection Details". Nu ar trebui să dureze mai mult de câteva minute să-ți faci un cont și să obții URL-ul.

Definirea schemei bazei de date

Aici e punctul unde Drizzle chiar strălucește. Schema ta e cod TypeScript pur — fiecare tabel, coloană și relație e definită cu funcții helper care mapează direct pe tipuri SQL.

Schema pentru un proiect de gestionare a proiectelor

Vom construi o aplicație simplă de gestionare a proiectelor cu două tabele: projects și tasks. Creează fișierul src/db/schema.ts:

import {
  pgTable,
  serial,
  varchar,
  text,
  timestamp,
  boolean,
  integer,
} from "drizzle-orm/pg-core";

export const projects = pgTable("projects", {
  id: serial("id").primaryKey(),
  name: varchar("name", { length: 256 }).notNull(),
  description: text("description"),
  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: 500 }).notNull(),
  description: text("description"),
  completed: boolean("completed").default(false).notNull(),
  projectId: integer("project_id")
    .references(() => projects.id, { onDelete: "cascade" })
    .notNull(),
  createdAt: timestamp("created_at").defaultNow().notNull(),
  updatedAt: timestamp("updated_at").defaultNow().notNull(),
});

Câteva lucruri de observat:

  • serial("id").primaryKey() — creează o coloană auto-increment ca cheie primară
  • varchar("name", { length: 256 }) — string cu lungime maximă specificată
  • .references(() => projects.id, { onDelete: "cascade" }) — cheie externă cu ștergere în cascadă (ștergerea unui proiect va elimina automat task-urile asociate)
  • timestamp("created_at").defaultNow().notNull() — timestamp cu valoare implicită la momentul inserării

Tipurile TypeScript sunt generate automat din această schemă. Nicio comandă extra, niciun pas intermediar. Poți extrage tipurile pentru insert și select așa:

import { InferSelectModel, InferInsertModel } from "drizzle-orm";

export type Project = InferSelectModel<typeof projects>;
export type NewProject = InferInsertModel<typeof projects>;
export type Task = InferSelectModel<typeof tasks>;
export type NewTask = InferInsertModel<typeof tasks>;

Configurarea conexiunii la baza de date

Creează fișierul src/db/index.ts pentru instanța Drizzle:

Varianta 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 });

Varianta PostgreSQL local

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

const client = postgres(process.env.DATABASE_URL!);

export const db = drizzle(client, { schema });

Parametrul { schema } e esențial aici — permite folosirea Relational Queries API, care oferă o sintaxă elegantă pentru interogări cu relații (similar cu include din Prisma, dar fără overhead-ul suplimentar).

Configurarea Drizzle Kit și migrări

Creează fișierul drizzle.config.ts în rădăcina proiectului:

import { defineConfig } from "drizzle-kit";

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

Acum poți genera și aplica migrările:

# Generează fișierele SQL de migrare
npx drizzle-kit generate

# Aplică migrările în baza de date
npx drizzle-kit migrate

Pentru dezvoltare rapidă, poți folosi și push care aplică schema direct, fără a genera fișiere SQL intermediare:

# Push direct — ideal pentru prototipare rapidă
npx drizzle-kit push

Recomandare: Folosește generate + migrate pentru producție (migrări versionate pe care le ții în Git) și push pentru dezvoltare locală când vrei să iterezi rapid.

Crearea schemelor de validare Zod

Înainte de a scrie Server Actions, hai să definim schemele Zod pentru validarea datelor. Creează fișierul src/lib/validators.ts:

import { z } from "zod";

export const createProjectSchema = z.object({
  name: z
    .string()
    .min(1, "Numele proiectului este obligatoriu")
    .max(256, "Numele nu poate depăși 256 de caractere"),
  description: z
    .string()
    .max(2000, "Descrierea nu poate depăși 2000 de caractere")
    .optional(),
});

export const updateProjectSchema = z.object({
  id: z.number().positive(),
  name: z
    .string()
    .min(1, "Numele proiectului este obligatoriu")
    .max(256, "Numele nu poate depăși 256 de caractere"),
  description: z
    .string()
    .max(2000, "Descrierea nu poate depăși 2000 de caractere")
    .optional(),
});

export const createTaskSchema = z.object({
  title: z
    .string()
    .min(1, "Titlul task-ului este obligatoriu")
    .max(500, "Titlul nu poate depăși 500 de caractere"),
  description: z
    .string()
    .max(2000, "Descrierea nu poate depăși 2000 de caractere")
    .optional(),
  projectId: z.number().positive("Proiectul este obligatoriu"),
});

export type CreateProjectInput = z.infer<typeof createProjectSchema>;
export type UpdateProjectInput = z.infer<typeof updateProjectSchema>;
export type CreateTaskInput = z.infer<typeof createTaskSchema>;

Prin definirea schemelor Zod separat, le poți reutiliza atât pe server (în Server Actions), cât și pe client (în formulare). Fără duplicare de cod, fără inconsistențe — o singură sursă de adevăr.

Server Actions pentru operațiuni CRUD

Hai să trecem la partea centrală: Server Actions care fac efectiv treaba cu baza de date. Creează fișierul src/actions/projects.ts:

CREATE — Adăugarea unui proiect nou

"use server";

import { db } from "@/db";
import { projects } from "@/db/schema";
import { createProjectSchema } from "@/lib/validators";
import { revalidatePath } from "next/cache";

export async function createProject(formData: FormData) {
  const raw = {
    name: formData.get("name") as string,
    description: formData.get("description") as string || undefined,
  };

  const validated = createProjectSchema.safeParse(raw);

  if (!validated.success) {
    return {
      success: false,
      errors: validated.error.flatten().fieldErrors,
    };
  }

  try {
    const [newProject] = await db
      .insert(projects)
      .values({
        name: validated.data.name,
        description: validated.data.description ?? null,
      })
      .returning();

    revalidatePath("/projects");

    return { success: true, data: newProject };
  } catch (error) {
    return {
      success: false,
      errors: { _form: ["Eroare la crearea proiectului. Încercați din nou."] },
    };
  }
}

Câteva detalii importante despre ce se întâmplă aici:

  • "use server" — directiva care marchează fișierul ca Server Action
  • safeParse — validează datele fără a arunca excepții, returnând un rezultat tipat
  • .returning() — funcție specifică PostgreSQL care returnează rândul inserat (nu funcționează cu MySQL, atenție)
  • revalidatePath("/projects") — invalidează cache-ul pentru ruta specificată, forțând Next.js să refetcheze datele

READ — Citirea proiectelor

"use server";

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

export async function getProjects() {
  return db.query.projects.findMany({
    orderBy: [desc(projects.createdAt)],
  });
}

export async function getProjectById(id: number) {
  return db.query.projects.findFirst({
    where: eq(projects.id, id),
  });
}

export async function getProjectWithTasks(id: number) {
  return db.query.projects.findFirst({
    where: eq(projects.id, id),
    with: {
      tasks: {
        orderBy: [desc(tasks.createdAt)],
      },
    },
  });
}

API-ul Relational Queries (db.query) e elegant și intuitiv. Funcția with îți permite să incluzi relațiile fără join-uri scrise manual — similar cu include din Prisma, dar fără overhead-ul suplimentar. Dacă ai lucrat cu Prisma până acum, o să-ți fie foarte familiar.

Notă importantă: Pentru a folosi db.query cu relații, trebuie să definești relațiile explicit în schemă. Adaugă în src/db/schema.ts:

import { relations } from "drizzle-orm";

export const projectsRelations = relations(projects, ({ many }) => ({
  tasks: many(tasks),
}));

export const tasksRelations = relations(tasks, ({ one }) => ({
  project: one(projects, {
    fields: [tasks.projectId],
    references: [projects.id],
  }),
}));

UPDATE — Actualizarea unui proiect

"use server";

import { db } from "@/db";
import { projects } from "@/db/schema";
import { updateProjectSchema } from "@/lib/validators";
import { eq } from "drizzle-orm";
import { revalidatePath } from "next/cache";

export async function updateProject(formData: FormData) {
  const raw = {
    id: Number(formData.get("id")),
    name: formData.get("name") as string,
    description: formData.get("description") as string || undefined,
  };

  const validated = updateProjectSchema.safeParse(raw);

  if (!validated.success) {
    return {
      success: false,
      errors: validated.error.flatten().fieldErrors,
    };
  }

  try {
    const [updated] = await db
      .update(projects)
      .set({
        name: validated.data.name,
        description: validated.data.description ?? null,
        updatedAt: new Date(),
      })
      .where(eq(projects.id, validated.data.id))
      .returning();

    revalidatePath("/projects");

    return { success: true, data: updated };
  } catch (error) {
    return {
      success: false,
      errors: { _form: ["Eroare la actualizarea proiectului."] },
    };
  }
}

DELETE — Ștergerea unui proiect

"use server";

import { db } from "@/db";
import { projects } from "@/db/schema";
import { eq } from "drizzle-orm";
import { revalidatePath } from "next/cache";

export async function deleteProject(id: number) {
  try {
    await db.delete(projects).where(eq(projects.id, id));
    revalidatePath("/projects");
    return { success: true };
  } catch (error) {
    return {
      success: false,
      error: "Eroare la ștergerea proiectului.",
    };
  }
}

Datorită onDelete: "cascade" pe care l-am definit în schemă, toate task-urile asociate unui proiect vor fi șterse automat. Nu trebuie să te preocupi de orfani în baza de date.

Server Actions pentru task-uri

Creează fișierul src/actions/tasks.ts pentru operațiunile pe task-uri:

"use server";

import { db } from "@/db";
import { tasks } from "@/db/schema";
import { createTaskSchema } from "@/lib/validators";
import { eq } from "drizzle-orm";
import { revalidatePath } from "next/cache";

export async function createTask(formData: FormData) {
  const raw = {
    title: formData.get("title") as string,
    description: formData.get("description") as string || undefined,
    projectId: Number(formData.get("projectId")),
  };

  const validated = createTaskSchema.safeParse(raw);

  if (!validated.success) {
    return {
      success: false,
      errors: validated.error.flatten().fieldErrors,
    };
  }

  try {
    const [newTask] = await db
      .insert(tasks)
      .values({
        title: validated.data.title,
        description: validated.data.description ?? null,
        projectId: validated.data.projectId,
      })
      .returning();

    revalidatePath("/projects/" + validated.data.projectId);

    return { success: true, data: newTask };
  } catch (error) {
    return {
      success: false,
      errors: { _form: ["Eroare la crearea task-ului."] },
    };
  }
}

export async function toggleTaskComplete(id: number, projectId: number) {
  const task = await db.query.tasks.findFirst({
    where: eq(tasks.id, id),
  });

  if (!task) {
    return { success: false, error: "Task-ul nu a fost găsit." };
  }

  try {
    await db
      .update(tasks)
      .set({
        completed: !task.completed,
        updatedAt: new Date(),
      })
      .where(eq(tasks.id, id));

    revalidatePath("/projects/" + projectId);

    return { success: true };
  } catch (error) {
    return { success: false, error: "Eroare la actualizarea task-ului." };
  }
}

export async function deleteTask(id: number, projectId: number) {
  try {
    await db.delete(tasks).where(eq(tasks.id, id));
    revalidatePath("/projects/" + projectId);
    return { success: true };
  } catch (error) {
    return { success: false, error: "Eroare la ștergerea task-ului." };
  }
}

Construirea interfeței cu Server Components

Acum că avem Server Actions funcționale, hai să construim și interfața. Un lucru frumos la Next.js 16 e că toate componentele sunt Server Components implicit — ceea ce înseamnă că poți interoga baza de date direct din componente, fără API routes intermediare.

Pagina de listare a proiectelor

Creează src/app/projects/page.tsx:

import { getProjects } from "@/actions/projects";
import { createProject } from "@/actions/projects";
import Link from "next/link";

export default async function ProjectsPage() {
  const projectsList = await getProjects();

  return (
    <div className="max-w-4xl mx-auto p-6">
      <h1 className="text-3xl font-bold mb-8">Proiectele mele</h1>

      <form action={createProject} className="mb-8 space-y-4">
        <input
          type="text"
          name="name"
          placeholder="Numele proiectului"
          required
          className="w-full p-3 border rounded-lg"
        />
        <textarea
          name="description"
          placeholder="Descriere (opțional)"
          className="w-full p-3 border rounded-lg"
          rows={3}
        />
        <button
          type="submit"
          className="bg-blue-600 text-white px-6 py-2 rounded-lg
                     hover:bg-blue-700 transition-colors"
        >
          Creează proiect
        </button>
      </form>

      <div className="space-y-4">
        {projectsList.map((project) => (
          <Link
            key={project.id}
            href={`/projects/${project.id}`}
            className="block p-4 border rounded-lg
                       hover:bg-gray-50 transition-colors"
          >
            <h2 className="text-xl font-semibold">{project.name}</h2>
            {project.description && (
              <p className="text-gray-600 mt-1">{project.description}</p>
            )}
            <p className="text-sm text-gray-400 mt-2">
              Creat: {project.createdAt.toLocaleDateString("ro-RO")}
            </p>
          </Link>
        ))}
      </div>
    </div>
  );
}

Observă cum getProjects() e apelat direct în componenta async — fără useEffect, fără useState, fără API route. Componenta e randată pe server, datele sunt fetchuite pe server, iar HTML-ul complet ajunge la client. E atât de simplu încât aproape pare suspect.

Pagina de detaliu a unui proiect

Creează src/app/projects/[id]/page.tsx:

import { getProjectWithTasks } from "@/actions/projects";
import { deleteProject } from "@/actions/projects";
import { createTask, toggleTaskComplete, deleteTask } from "@/actions/tasks";
import { notFound, redirect } from "next/navigation";

export default async function ProjectDetailPage({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params;
  const project = await getProjectWithTasks(Number(id));

  if (!project) {
    notFound();
  }

  async function handleDelete() {
    "use server";
    await deleteProject(project!.id);
    redirect("/projects");
  }

  return (
    <div className="max-w-4xl mx-auto p-6">
      <h1 className="text-3xl font-bold">{project.name}</h1>
      {project.description && (
        <p className="text-gray-600 mt-2">{project.description}</p>
      )}

      <form action={handleDelete} className="mt-4">
        <button
          type="submit"
          className="text-red-600 hover:text-red-800 text-sm"
        >
          Șterge proiectul
        </button>
      </form>

      <h2 className="text-2xl font-semibold mt-8 mb-4">Task-uri</h2>

      <form action={createTask} className="mb-6 space-y-3">
        <input type="hidden" name="projectId" value={project.id} />
        <input
          type="text"
          name="title"
          placeholder="Titlu task nou"
          required
          className="w-full p-3 border rounded-lg"
        />
        <button
          type="submit"
          className="bg-green-600 text-white px-4 py-2 rounded-lg
                     hover:bg-green-700 transition-colors"
        >
          Adaugă task
        </button>
      </form>

      <ul className="space-y-2">
        {project.tasks?.map((task) => (
          <li
            key={task.id}
            className="flex items-center justify-between p-3
                       border rounded-lg"
          >
            <div className="flex items-center gap-3">
              <form
                action={async () => {
                  "use server";
                  await toggleTaskComplete(task.id, project.id);
                }}
              >
                <button type="submit">
                  {task.completed ? "✓" : "○"}
                </button>
              </form>
              <span className={task.completed ? "line-through text-gray-400" : ""}>
                {task.title}
              </span>
            </div>
            <form
              action={async () => {
                "use server";
                await deleteTask(task.id, project.id);
              }}
            >
              <button
                type="submit"
                className="text-red-500 hover:text-red-700 text-sm"
              >
                Șterge
              </button>
            </form>
          </li>
        ))}
      </ul>
    </div>
  );
}

Gestionarea stării formularelor cu useActionState

Ce am arătat până acum funcționează, dar utilizatorul nu primește prea mult feedback vizual. Hai să îmbunătățim asta cu useActionState din React 19 (inclus în Next.js 16). Hook-ul ăsta gestionează starea acțiunii și oferă mesaje de eroare sau succes automat.

Creează o componentă client src/components/CreateProjectForm.tsx:

"use client";

import { useActionState } from "react";
import { useFormStatus } from "react-dom";
import { createProject } from "@/actions/projects";

function SubmitButton() {
  const { pending } = useFormStatus();

  return (
    <button
      type="submit"
      disabled={pending}
      className="bg-blue-600 text-white px-6 py-2 rounded-lg
                 hover:bg-blue-700 disabled:opacity-50
                 transition-colors"
    >
      {pending ? "Se creează..." : "Creează proiect"}
    </button>
  );
}

export function CreateProjectForm() {
  const [state, formAction] = useActionState(createProject, null);

  return (
    <form action={formAction} className="space-y-4">
      <div>
        <input
          type="text"
          name="name"
          placeholder="Numele proiectului"
          className="w-full p-3 border rounded-lg"
        />
        {state?.errors?.name && (
          <p className="text-red-500 text-sm mt-1">
            {state.errors.name[0]}
          </p>
        )}
      </div>

      <textarea
        name="description"
        placeholder="Descriere (opțional)"
        className="w-full p-3 border rounded-lg"
        rows={3}
      />

      <SubmitButton />

      {state?.success && (
        <p className="text-green-600">Proiectul a fost creat cu succes!</p>
      )}
    </form>
  );
}

useActionState primește Server Action-ul și o stare inițială, apoi returnează starea curentă și o funcție formAction pe care o atribui formularului. Între timp, useFormStatus oferă starea pending — utilă pentru a dezactiva butonul și a arăta utilizatorului că se întâmplă ceva.

Optimizarea performanței cu Drizzle

Drizzle vine cu câteva tehnici de optimizare care merită cunoscute, mai ales în contextul Next.js 16:

Selectarea doar a coloanelor necesare

// In loc de a selecta toate coloanele
const allData = await db.select().from(projects);

// Selecteaza doar ce ai nevoie
const names = await db
  .select({
    id: projects.id,
    name: projects.name,
  })
  .from(projects);

Pare un detaliu minor, dar când ai tabele cu multe coloane sau volume mari de date, diferența se simte — mai ales pe conexiuni serverless unde fiecare byte conteaz.

Paginare eficientă

import { desc } from "drizzle-orm";

export async function getProjectsPaginated(page: number, limit: number = 10) {
  return db
    .select()
    .from(projects)
    .orderBy(desc(projects.createdAt))
    .limit(limit)
    .offset((page - 1) * limit);
}

Prepared Statements

import { eq, placeholder } from "drizzle-orm";

const getProjectQuery = db.query.projects
  .findFirst({
    where: eq(projects.id, placeholder("id")),
  })
  .prepare("get_project_by_id");

// Utilizare — query-ul este pre-compilat si reutilizabil
const project = await getProjectQuery.execute({ id: 42 });

Prepared Statements sunt deosebit de utile în medii serverless — reduc timpul de parsare a query-urilor la apelurile repetate. E una dintre acele optimizări care nu necesită mult efort dar aduce beneficii reale.

Structura completă a proiectului

La finalul acestui tutorial, structura proiectului tău ar trebui să arate cam așa:

my-crud-app/
├── drizzle/                    # Fișiere de migrare generate
│   └── 0000_initial.sql
├── drizzle.config.ts           # Configurare Drizzle Kit
├── src/
│   ├── actions/
│   │   ├── projects.ts         # Server Actions pentru proiecte
│   │   └── tasks.ts            # Server Actions pentru task-uri
│   ├── app/
│   │   ├── projects/
│   │   │   ├── page.tsx        # Lista de proiecte
│   │   │   └── [id]/
│   │   │       └── page.tsx    # Detaliu proiect cu task-uri
│   │   ├── layout.tsx
│   │   └── page.tsx
│   ├── components/
│   │   └── CreateProjectForm.tsx
│   ├── db/
│   │   ├── index.ts            # Instanța Drizzle (conexiune DB)
│   │   └── schema.ts           # Schema tabelelor + relații
│   └── lib/
│       └── validators.ts       # Scheme Zod partajate
├── .env.local
├── package.json
└── tsconfig.json

Comenzi utile pentru dezvoltare

Ca referință rapidă, iată comenzile pe care le vei folosi cel mai des:

# Pornește serverul de dezvoltare (Turbopack implicit în Next.js 16)
npm run dev

# Generează migrări din schema modificată
npx drizzle-kit generate

# Aplică migrările în baza de date
npx drizzle-kit migrate

# Push direct al schemei (dezvoltare rapidă)
npx drizzle-kit push

# Deschide Drizzle Studio — interfață vizuală pentru baza de date
npx drizzle-kit studio

Drizzle Studio merită menționat separat — e un instrument vizual care-ți permite să navighezi și să editezi datele direct din browser. Foarte util când vrei să verifici rapid ce e în baza de date fără să scrii query-uri.

Întrebări frecvente

De ce trebuie să folosesc Server Actions pentru mutațiile Drizzle în loc de API routes?

Conexiunea la baza de date (instanța Drizzle) există doar pe server — nu poate fi accesată din codul client. Server Actions sunt cel mai direct mod de a efectua mutații din componente, fără a crea API routes intermediare. Funcționează nativ cu formulare HTML și oferă type safety end-to-end. API routes rămân o opțiune validă dacă preferi, dar Server Actions simplifică semnificativ fluxul de date.

Pot folosi Drizzle ORM pe Vercel Edge Runtime?

Da, și sincer acesta e unul dintre marile avantaje ale Drizzle. Cu un footprint de ~33KB și fără motor binar, funcționează nativ pe Edge Runtime — inclusiv Vercel Edge Functions, Cloudflare Workers și Deno Deploy. Folosind driver-ul @neondatabase/serverless cu protocolul HTTP, poți executa query-uri de pe edge fără conexiuni TCP tradiționale.

Cum gestionez erorile de bază de date în Server Actions?

Folosește try/catch în jurul operațiunilor Drizzle și returnează obiecte de eroare structurate. Validarea cu Zod (safeParse) prinde erorile de validare înainte de a ajunge la baza de date, iar try/catch gestionează erorile de conectivitate sau constrângeri. Regula de bază: nu arunca niciodată excepții din Server Actions — returnează întotdeauna obiecte cu câmpuri success și errors.

Care e diferența între drizzle-kit push și drizzle-kit migrate?

push aplică schema direct în baza de date, fără a genera fișiere SQL intermediare — perfect pentru prototipare rapidă. generate + migrate creează fișiere SQL versionate pe care le poți stoca în Git și aplica în CI/CD — recomandat pentru producție. Pe scurt: push pentru dezvoltare, migrate pentru producție.

Trebuie să apelez revalidatePath după fiecare mutație?

Da, dacă vrei ca interfața să reflecte imediat modificările. Next.js 16 cache-uiește agresiv rezultatele Server Components — fără revalidatePath, utilizatorul ar vedea date vechi până la o navigare completă. Ca alternativă, poți folosi revalidateTag pentru o invalidare mai granulară, util mai ales când ai mai multe rute care afișează aceleași date.

Despre Autor Editorial Team

Our team of expert writers and editors.