Γιατί το Drizzle ORM Αλλάζει τα Δεδομένα του Παιχνιδιού
Ας πούμε τα πράγματα όπως είναι: η σύνδεση μιας βάσης δεδομένων με μια Next.js εφαρμογή ήταν πάντα λίγο... ταλαιπωρία. Για χρόνια, το Prisma ήταν η go-to επιλογή στο οικοσύστημα TypeScript. Δούλευε, αλλά είχε τα θέματά του — ειδικά αν δούλευες σε serverless περιβάλλοντα.
Εδώ μπαίνει στο παιχνίδι το Drizzle ORM.
Με μέγεθος bundle μόλις ~7.4KB, μηδενικές runtime εξαρτήσεις και native υποστήριξη edge runtime, το Drizzle έχει γίνει η πρώτη επιλογή για σύγχρονες serverless εφαρμογές. Και ειλικρινά, μόλις το δοκιμάσεις, είναι δύσκολο να γυρίσεις πίσω.
Σε αυτόν τον οδηγό θα φτιάξουμε μαζί μια πλήρη full-stack εφαρμογή με Next.js App Router, Drizzle ORM και PostgreSQL (μέσω Neon). Θα καλύψουμε τα πάντα — εγκατάσταση, CRUD operations με Server Actions, validation με Zod, relational queries. Ό,τι χρειάζεσαι για να ξεκινήσεις.
Drizzle ORM vs Prisma: Γιατί να Αλλάξεις
Πριν μπούμε στον κώδικα, αξίζει να δούμε γιατί το Drizzle υπερτερεί σε συγκεκριμένα σενάρια. Οι αριθμοί μιλάνε μόνοι τους:
| Χαρακτηριστικό | Drizzle ORM | Prisma |
|---|---|---|
| Μέγεθος Bundle | ~7.4KB (min+gzip) | ~1.6MB |
| Cold Start (Serverless) | ~400ms | ~1100ms |
| Edge Runtime | Native υποστήριξη | Απαιτεί Prisma Accelerate |
| Code Generation | Δεν χρειάζεται | Απαιτείται prisma generate |
| Σύνταξη | SQL-like TypeScript | Αφαιρετική DSL |
| Κόστος | 100% δωρεάν & open source | Δωρεάν core, πληρωμή για Accelerate |
Αν κάνεις deploy σε Vercel Functions ή Cloudflare Workers, αυτή η διαφορά 700ms στο cold start είναι κάτι που οι χρήστες σου πραγματικά αισθάνονται. Δεν είναι θεωρητική — είναι πραγματική.
Εγκατάσταση και Ρύθμιση Project
Δημιουργία Next.js Project
Ξεκινάμε με ένα καθαρό Next.js project:
npx create-next-app@latest my-drizzle-app
cd my-drizzle-app
Στις ερωτήσεις, επιλέξτε Yes για TypeScript, Tailwind CSS και App Router. Τα υπόλοιπα κατά προτίμηση.
Εγκατάσταση Drizzle ORM
Τώρα εγκαθιστούμε το Drizzle ORM μαζί με τον Neon serverless driver:
npm install drizzle-orm @neondatabase/serverless
npm install -D drizzle-kit
Και φυσικά το Zod για validation:
npm install zod
Ρύθμιση Neon PostgreSQL
Το Neon είναι μια serverless PostgreSQL πλατφόρμα που ταιριάζει γάντι με το Drizzle. Δημιουργήστε δωρεάν λογαριασμό στο neon.tech και αντιγράψτε το connection string.
Δημιουργήστε ένα αρχείο .env.local στη ρίζα του project:
DATABASE_URL=postgresql://user:[email protected]/mydb?sslmode=require
Ορισμός Schema σε TypeScript
Εδώ είναι που αρχίζει η μαγεία. Ένα από τα πιο ωραία πράγματα στο Drizzle είναι ότι ορίζεις το schema απευθείας σε TypeScript. Κανένα ξεχωριστό αρχείο schema, κανένα code generation. Απλά γράφεις TypeScript και δουλεύει.
Δημιουργήστε τον φάκελο src/db και μέσα σε αυτόν το αρχείο schema:
// src/db/schema.ts
import {
pgTable,
serial,
text,
varchar,
timestamp,
integer,
boolean,
} from "drizzle-orm/pg-core";
import { relations } from "drizzle-orm";
// Πίνακας χρηστών
export const users = pgTable("users", {
id: serial("id").primaryKey(),
name: varchar("name", { length: 100 }).notNull(),
email: varchar("email", { length: 255 }).notNull().unique(),
createdAt: timestamp("created_at").defaultNow().notNull(),
});
// Πίνακας άρθρων
export const posts = pgTable("posts", {
id: serial("id").primaryKey(),
title: varchar("title", { length: 200 }).notNull(),
content: text("content"),
published: boolean("published").default(false).notNull(),
authorId: integer("author_id")
.references(() => users.id)
.notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().notNull(),
});
// Πίνακας tags (για many-to-many σχέση)
export const tags = pgTable("tags", {
id: serial("id").primaryKey(),
name: varchar("name", { length: 50 }).notNull().unique(),
});
// Junction table για posts ↔ tags
export const postsTags = pgTable("posts_tags", {
postId: integer("post_id")
.references(() => posts.id)
.notNull(),
tagId: integer("tag_id")
.references(() => tags.id)
.notNull(),
});
Κάθε πεδίο είναι πλήρως typed. Αν γράψεις λάθος τύπο ή ξεχάσεις κάποιο required πεδίο, ο TypeScript compiler θα σε πιάσει αμέσως. Αυτό από μόνο του αξίζει πολύ — ειδικά σε μεγαλύτερα projects.
Σχέσεις (Relations) στο Drizzle
Το Drizzle υποστηρίζει relational queries που παράγουν πάντα ένα μόνο SQL query. Αυτό είναι τεράστιο σε serverless περιβάλλοντα όπου κάθε roundtrip στη βάση μετράει. Ας ορίσουμε τις σχέσεις:
// src/db/schema.ts (συνέχεια)
// One-to-Many: User → Posts
export const usersRelations = relations(users, ({ many }) => ({
posts: many(posts),
}));
// Many-to-One: Post → User
export const postsRelations = relations(posts, ({ one, many }) => ({
author: one(users, {
fields: [posts.authorId],
references: [users.id],
}),
postsTags: many(postsTags),
}));
// Junction table relations
export const postsTagsRelations = relations(postsTags, ({ one }) => ({
post: one(posts, {
fields: [postsTags.postId],
references: [posts.id],
}),
tag: one(tags, {
fields: [postsTags.tagId],
references: [tags.id],
}),
}));
export const tagsRelations = relations(tags, ({ many }) => ({
postsTags: many(postsTags),
}));
Σύνδεση με τη Βάση Δεδομένων
Η σύνδεση με τη βάση γίνεται σε δύο γραμμές (κυριολεκτικά):
// src/db/index.ts
import { neon } from "@neondatabase/serverless";
import { drizzle } from "drizzle-orm/neon-http";
import * as schema from "./schema";
const sql = neon(process.env.DATABASE_URL!);
export const db = drizzle(sql, { schema });
Μην ξεχάσετε να περάσετε το schema στο drizzle() — αλλιώς δεν θα δουλέψει το relational query API (db.query). Κλασικό λάθος που κάνουν πολλοί στην αρχή.
Configuration και Migrations
Drizzle Config
Δημιουργήστε το αρχείο drizzle.config.ts στη ρίζα του project:
// drizzle.config.ts
import { defineConfig } from "drizzle-kit";
export default defineConfig({
schema: "./src/db/schema.ts",
out: "./drizzle",
dialect: "postgresql",
dbCredentials: {
url: process.env.DATABASE_URL!,
},
});
Εκτέλεση Migrations
Προσθέστε αυτά τα scripts στο package.json:
{
"scripts": {
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate",
"db:push": "drizzle-kit push",
"db:studio": "drizzle-kit studio"
}
}
Και τρέξτε τα migrations:
npm run db:generate
npm run db:push
Η db:generate παράγει SQL migration αρχεία βάσει του schema σας, ενώ η db:push τα εφαρμόζει στη βάση. Για production, προτιμήστε db:migrate αντί για db:push — δίνει περισσότερο έλεγχο στις αλλαγές.
Αν θέλετε να δείτε τι γίνεται στη βάση σας οπτικά, δοκιμάστε το Drizzle Studio:
npm run db:studio
Είναι δωρεάν και, ειλικρινά, αρκετά βολικό για debugging.
Data Fetching σε Server Components
Λοιπόν, εδώ αρχίζουν τα ενδιαφέροντα. Στο Next.js App Router, τα Server Components τρέχουν στον server, που σημαίνει ότι μπορείς να κάνεις queries κατευθείαν χωρίς κανένα API layer ενδιάμεσα:
// src/app/posts/page.tsx
import { db } from "@/db";
import { posts, users } from "@/db/schema";
import { desc, eq } from "drizzle-orm";
export default async function PostsPage() {
const allPosts = await db
.select({
id: posts.id,
title: posts.title,
published: posts.published,
authorName: users.name,
createdAt: posts.createdAt,
})
.from(posts)
.leftJoin(users, eq(posts.authorId, users.id))
.where(eq(posts.published, true))
.orderBy(desc(posts.createdAt));
return (
<div>
<h1>Άρθρα</h1>
{allPosts.map((post) => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>Από: {post.authorName}</p>
</article>
))}
</div>
);
}
Αν έρχεσαι από Prisma, η σύνταξη μοιάζει πιο πολύ με SQL — κάτι που προσωπικά βρίσκω πιο ξεκάθαρο. Ξέρεις ακριβώς τι query θα φύγει στη βάση.
Relational Queries με db.query
Αν προτιμάς πιο αφαιρετική σύνταξη για nested data, μπορείς να χρησιμοποιήσεις το relational query API:
// Φέρνουμε χρήστες μαζί με τα posts τους
const usersWithPosts = await db.query.users.findMany({
with: {
posts: {
where: (posts, { eq }) => eq(posts.published, true),
orderBy: (posts, { desc }) => [desc(posts.createdAt)],
limit: 5,
},
},
});
// Φέρνουμε ένα post μαζί με τον author και τα tags
const postWithDetails = await db.query.posts.findFirst({
where: (posts, { eq }) => eq(posts.id, postId),
with: {
author: true,
postsTags: {
with: {
tag: true,
},
},
},
});
Και εδώ είναι το κλειδί: το Drizzle παράγει ένα μόνο SQL query για κάθε κλήση. Κανένα N+1 πρόβλημα. Κανένα περιττό roundtrip. Αυτό κάνει τεράστια διαφορά σε serverless environments.
CRUD Operations με Server Actions
Ας φτιάξουμε ένα πλήρες σύστημα CRUD χρησιμοποιώντας Server Actions. Αυτός ο τρόπος αντικαθιστά πρακτικά τα API routes για mutations, και είναι πολύ πιο καθαρός.
Create — Δημιουργία Εγγραφής
// src/app/actions/posts.ts
"use server";
import { db } from "@/db";
import { posts } from "@/db/schema";
import { revalidatePath } from "next/cache";
import { z } from "zod";
const createPostSchema = z.object({
title: z.string().min(1, "Ο τίτλος είναι υποχρεωτικός").max(200),
content: z.string().optional(),
authorId: z.coerce.number().positive(),
});
export type ActionState = {
message: string;
errors?: Record<string, string[]>;
success?: boolean;
};
export async function createPost(
prevState: ActionState,
formData: FormData
): Promise<ActionState> {
const validated = createPostSchema.safeParse({
title: formData.get("title"),
content: formData.get("content"),
authorId: formData.get("authorId"),
});
if (!validated.success) {
return {
message: "Σφάλμα επικύρωσης",
errors: validated.error.flatten().fieldErrors,
};
}
await db.insert(posts).values({
title: validated.data.title,
content: validated.data.content ?? null,
authorId: validated.data.authorId,
});
revalidatePath("/posts");
return { message: "Το άρθρο δημιουργήθηκε!", success: true };
}
Read — Ανάγνωση Εγγραφής
import { eq } from "drizzle-orm";
export async function getPost(id: number) {
const post = await db.query.posts.findFirst({
where: (posts, { eq }) => eq(posts.id, id),
with: { author: true },
});
return post;
}
Update — Ενημέρωση Εγγραφής
export async function updatePost(
prevState: ActionState,
formData: FormData
): Promise<ActionState> {
const id = Number(formData.get("id"));
const title = formData.get("title") as string;
const content = formData.get("content") as string;
await db
.update(posts)
.set({
title,
content,
updatedAt: new Date(),
})
.where(eq(posts.id, id));
revalidatePath("/posts");
return { message: "Το άρθρο ενημερώθηκε!", success: true };
}
Delete — Διαγραφή Εγγραφής
export async function deletePost(id: number) {
await db.delete(posts).where(eq(posts.id, id));
revalidatePath("/posts");
}
Toggle Published Status
import { not } from "drizzle-orm";
export async function togglePublished(id: number) {
await db
.update(posts)
.set({ published: not(posts.published) })
.where(eq(posts.id, id));
revalidatePath("/posts");
}
Απλό, καθαρό, type-safe. Κανένα boilerplate.
Validation με Zod Integration
Ένα πράγμα που βαριέσαι να κάνεις (και δικαίως) είναι να γράφεις Zod schemas χειροκίνητα όταν ήδη έχεις ορίσει τα πεδία στο Drizzle schema. Η λύση; Αυτόματη παραγωγή schemas:
// src/db/validation.ts
import { createInsertSchema, createSelectSchema } from "drizzle-orm/zod";
import { posts, users } from "./schema";
// Αυτόματη παραγωγή insert schema
export const insertPostSchema = createInsertSchema(posts, {
title: (schema) =>
schema.min(1, "Ο τίτλος είναι υποχρεωτικός").max(200),
});
// Αυτόματη παραγωγή select schema
export const selectPostSchema = createSelectSchema(posts);
// Insert schema για χρήστες
export const insertUserSchema = createInsertSchema(users, {
email: (schema) => schema.email("Μη έγκυρο email"),
name: (schema) =>
schema.min(2, "Το όνομα πρέπει να έχει τουλάχιστον 2 χαρακτήρες"),
});
// Εξαγωγή TypeScript τύπων
export type InsertPost = typeof insertPostSchema._type;
export type SelectPost = typeof selectPostSchema._type;
Αν αργότερα αλλάξεις ένα πεδίο στο schema (π.χ. μεγαλώσεις ένα varchar), τα validation rules ακολουθούν αυτόματα. Single source of truth — το ονειρό κάθε developer.
Client Component με useActionState
Ας φτιάξουμε τώρα μια φόρμα που χρησιμοποιεί τα Server Actions μας. Εδώ χρησιμοποιούμε το useActionState του React 19, που κάνει τη διαχείριση pending state και σφαλμάτων παιχνιδάκι:
// src/app/posts/new/PostForm.tsx
"use client";
import { useActionState } from "react";
import { createPost, type ActionState } from "@/app/actions/posts";
const initialState: ActionState = { message: "" };
export default function PostForm() {
const [state, formAction, isPending] = useActionState(
createPost,
initialState
);
return (
<form action={formAction}>
<div>
<label htmlFor="title">Τίτλος</label>
<input
id="title"
name="title"
type="text"
required
/>
{state.errors?.title && (
<p className="text-red-500">{state.errors.title[0]}</p>
)}
</div>
<div>
<label htmlFor="content">Περιεχόμενο</label>
<textarea id="content" name="content" rows={6} />
</div>
<input type="hidden" name="authorId" value="1" />
<button type="submit" disabled={isPending}>
{isPending ? "Αποθήκευση..." : "Δημοσίευση"}
</button>
{state.message && (
<p className={state.success ? "text-green-600" : "text-red-600"}>
{state.message}
</p>
)}
</form>
);
}
Η επικύρωση γίνεται server-side, αλλά τα μηνύματα σφάλματος εμφανίζονται κανονικά στον client. Το καλύτερο; Δεν χρειάζεται useState, useEffect ή χειροκίνητο fetch. Τα πάντα περνάνε μέσω του form action.
Edge Runtime Compatibility
Αυτό είναι ένα σημείο που ξεχωρίζει πολύ το Drizzle. Μπορείς να τρέξεις queries σε Vercel Edge Functions ή Cloudflare Workers χωρίς κανένα workaround:
// src/app/api/posts/route.ts
import { db } from "@/db";
import { posts } from "@/db/schema";
import { eq } from "drizzle-orm";
import { NextResponse } from "next/server";
export const runtime = "edge";
export async function GET() {
const allPosts = await db
.select()
.from(posts)
.where(eq(posts.published, true))
.limit(20);
return NextResponse.json(allPosts);
}
Ο Neon serverless driver επικοινωνεί μέσω HTTP αντί για TCP. Αυτό σημαίνει ότι δουλεύει σε edge environments χωρίς ιδιαίτερη ρύθμιση — κάτι που με το Prisma χρειάζεται πληρωμένο Accelerate proxy.
Δομή Project — Βέλτιστες Πρακτικές
Αφού φτάσαμε ως εδώ, ας δούμε μια καθαρή δομή φακέλων που δουλεύει καλά σε πραγματικά projects:
my-drizzle-app/
├── drizzle/ # Generated migrations
├── src/
│ ├── app/
│ │ ├── posts/
│ │ │ ├── page.tsx # Server Component - listing
│ │ │ ├── [id]/
│ │ │ │ └── page.tsx # Server Component - detail
│ │ │ └── new/
│ │ │ └── PostForm.tsx # Client Component - form
│ │ ├── actions/
│ │ │ └── posts.ts # Server Actions
│ │ └── api/
│ │ └── posts/
│ │ └── route.ts # Route Handler (Edge)
│ └── db/
│ ├── index.ts # Database connection
│ ├── schema.ts # Table definitions & relations
│ └── validation.ts # Zod schemas
├── drizzle.config.ts
├── .env.local
└── package.json
Η ιδέα είναι απλή: κράτα τα database-related αρχεία στο src/db, τα Server Actions στο src/app/actions, και ομαδοποίησε τα components κατά feature.
Συμβουλές για Production
Μερικά πράγματα που αξίζει να θυμάσαι πριν κάνεις deploy:
- Connection pooling: Το Neon παρέχει ενσωματωμένο pooling. Για τοπικό development, χρησιμοποίησε τον
postgresdriver αντί του Neon HTTP — είναι πιο γρήγορος σε local. - Strict mode στα migrations: Πάντα
strict: trueστο drizzle-kit. Αλλιώς ρισκάρεις data loss σε column renames (το μαθαίνεις με τον δύσκολο τρόπο). - Indexes: Πρόσθεσε indexes στα πεδία που χρησιμοποιείς σε WHERE και JOINs. Κάνει πραγματική διαφορά στο performance.
- Transactions: Χρησιμοποίησε
db.transaction()για operations που πρέπει να εκτελεστούν ατομικά. - Drizzle Studio: Τρέξε
npx drizzle-kit studioγια visual debugging. Δωρεάν και χρήσιμο.
Ένα γρήγορο παράδειγμα transaction:
// Παράδειγμα transaction
await db.transaction(async (tx) => {
const [newPost] = await tx
.insert(posts)
.values({ title: "Νέο Post", authorId: 1 })
.returning();
await tx.insert(postsTags).values([
{ postId: newPost.id, tagId: 1 },
{ postId: newPost.id, tagId: 2 },
]);
});
Συχνές Ερωτήσεις (FAQ)
Μπορώ να χρησιμοποιήσω το Drizzle ORM χωρίς PostgreSQL;
Ναι, σίγουρα. Το Drizzle υποστηρίζει PostgreSQL, MySQL, SQLite, καθώς και serverless βάσεις όπως Neon, PlanetScale, Turso και Cloudflare D1. Η σύνταξη μένει σχεδόν ίδια — αλλάζει μόνο ο driver και τα table helpers (π.χ. pgTable γίνεται sqliteTable).
Το Drizzle ORM είναι production-ready;
Απολύτως. Χρησιμοποιείται σε production από χιλιάδες εφαρμογές, έχει πάνω από 25.000 GitHub stars, και η Vercel το περιλαμβάνει στα επίσημα starter templates της. Αν αυτό δεν σε πείθει, δεν ξέρω τι θα σε πείσει.
Πώς κάνω migrate από Prisma σε Drizzle;
Χρησιμοποιήστε το drizzle-kit introspect για να δημιουργήσετε αυτόματα Drizzle schema από μια υπάρχουσα βάση. Μπορείτε να κάνετε τη μετάβαση σταδιακά, κρατώντας τα Prisma migrations σαν ιστορικό.
Χρειάζομαι code generation όπως στο Prisma;
Όχι, και αυτό είναι ένα τεράστιο πλεονέκτημα. Οι τύποι εξάγονται απευθείας από τον TypeScript κώδικα του schema σας. Δεν χρειάζεται prisma generate μετά από κάθε αλλαγή — οι τύποι ενημερώνονται αυτόματα καθώς γράφεις κώδικα.
Πώς δουλεύει το Drizzle με τα Server Actions του Next.js;
Τέλεια. Η σύνδεση db του Drizzle τρέχει πάντα server-side, οπότε λειτουργεί κανονικά μέσα σε Server Actions (με "use server"). Μετά από κάθε mutation, απλά καλείς revalidatePath() για να ανανεωθούν τα cached δεδομένα.