Next.js App Router'da Drizzle ORM ile Veritabanı Entegrasyonu: PostgreSQL, CRUD ve Edge Runtime

Drizzle ORM ile Next.js App Router üzerinde PostgreSQL veritabanı entegrasyonu nasıl yapılır? Şema tanımlama, migration, Server Components ile veri çekme, Server Actions ile CRUD işlemleri ve Edge Runtime desteğini adım adım öğrenin.

Drizzle ORM Nedir ve Neden Next.js için İdeal?

TypeScript dünyasında ORM seçimi her zaman tartışmalı bir konu olmuştur. Prisma mı, TypeORM mi, Knex mi... Ama son bir-iki yılda sahneye çıkan Drizzle ORM, bu denklemi ciddi şekilde değiştirdi. 2026 itibarıyla 25.000'den fazla GitHub yıldızına ulaşmış olan Drizzle, özellikle Next.js App Router ile birlikte kullanıldığında gerçekten etkileyici bir geliştirici deneyimi sunuyor.

Peki ne yapıyor bu Drizzle'ı bu kadar farklı? Kısaca: SQL biliyorsanız, Drizzle'ı zaten biliyorsunuz. Özel bir sorgu dili yok, kod üretme adımı yok ve çalışma zamanı yükü sıfır.

Drizzle ORM'nin Next.js App Router ile uyumunu özel kılan üç temel özellik var:

  • Sıfır çalışma zamanı yükü: Drizzle, sorgularınızı doğrudan SQL'e derler — arada ekstra bir sorgu motoru katmanı yoktur.
  • TypeScript-native şema: Şemanız TypeScript kodudur; ayrı bir .prisma dosyası veya generate komutu gerektirmez.
  • Edge Runtime desteği: Yalnızca ~7.4 KB boyutuyla Vercel Edge Runtime ve Cloudflare Workers üzerinde sorunsuz çalışır. Evet, doğru okudunuz — 7.4 KB.

Proje Kurulumu ve Bağımlılıklar

Haydi, işe koyulalım. Yeni bir Next.js projesi oluşturup Drizzle ORM'yi PostgreSQL ile entegre edeceğiz. İlk adım olarak gerekli paketleri yükleyelim:

npx create-next-app@latest my-drizzle-app --typescript --app --tailwind
cd my-drizzle-app

# Drizzle ORM ve PostgreSQL sürücüsü
npm install drizzle-orm postgres

# Drizzle Kit (geliştirme araçları)
npm install -D drizzle-kit

Buradaki postgres paketi, Node.js için hafif ve modern bir PostgreSQL istemcisi. Eğer Neon veya Supabase gibi sunucusuz bir PostgreSQL hizmeti kullanıyorsanız, ilgili sürücüyü de eklemeniz gerekecek:

# Neon sunucusuz PostgreSQL için
npm install @neondatabase/serverless

# Supabase için
npm install @supabase/supabase-js

Veritabanı Bağlantısı Yapılandırma

Proje kök dizininde bir .env.local dosyası oluşturup veritabanı URL'nizi tanımlayın:

DATABASE_URL="postgres://kullanici:sifre@localhost:5432/my_drizzle_db"

Şimdi veritabanı bağlantı dosyamızı oluşturalım. Bu dosya projenizin kalbi gibi düşünülebilir — tüm sorgularınız buradan geçecek:

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

const connectionString = process.env.DATABASE_URL!;

// Sorgu istemcisi (bağlantı havuzu ile)
const client = postgres(connectionString, {
  max: 10, // maksimum bağlantı sayısı
});

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

Neon kullanıyorsanız bağlantı yapılandırması biraz farklı olacak:

// src/db/index.ts (Neon için)
import { drizzle } from "drizzle-orm/neon-http";
import { neon } from "@neondatabase/serverless";
import * as schema from "./schema";

const sql = neon(process.env.DATABASE_URL!);
export const db = drizzle(sql, { schema });

Drizzle Yapılandırma Dosyası

Proje kök dizinine bir de drizzle.config.ts dosyası eklememiz gerekiyor. Bu dosya, Drizzle Kit'in şemanızı, migration çıktı klasörünü ve veritabanı bağlantınızı bilmesini sağlıyor:

// 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!,
  },
});

Yapılandırma bu kadar basit. Dört satır kod ve Drizzle Kit hazır.

Şema Tanımlama: TypeScript ile Veritabanı Modelleme

Drizzle'ın bence en güçlü tarafı şemanızı doğrudan TypeScript ile tanımlayabilmeniz. Ayrı bir DSL öğrenmenize gerek yok — sadece TypeScript yazıyorsunuz. Bir blog uygulaması için örnek şema oluşturalım:

// src/db/schema.ts
import {
  pgTable,
  serial,
  text,
  varchar,
  timestamp,
  integer,
  boolean,
} from "drizzle-orm/pg-core";
import { relations } from "drizzle-orm";

// Kullanıcılar tablosu
export const users = pgTable("users", {
  id: serial("id").primaryKey(),
  name: varchar("name", { length: 100 }).notNull(),
  email: varchar("email", { length: 255 }).unique().notNull(),
  avatarUrl: text("avatar_url"),
  createdAt: timestamp("created_at").defaultNow().notNull(),
});

// Yazılar tablosu
export const posts = pgTable("posts", {
  id: serial("id").primaryKey(),
  title: varchar("title", { length: 255 }).notNull(),
  content: text("content").notNull(),
  published: boolean("published").default(false).notNull(),
  authorId: integer("author_id")
    .references(() => users.id, { onDelete: "cascade" })
    .notNull(),
  createdAt: timestamp("created_at").defaultNow().notNull(),
  updatedAt: timestamp("updated_at").defaultNow().notNull(),
});

// Yorumlar tablosu
export const comments = pgTable("comments", {
  id: serial("id").primaryKey(),
  body: text("body").notNull(),
  postId: integer("post_id")
    .references(() => posts.id, { onDelete: "cascade" })
    .notNull(),
  authorId: integer("author_id")
    .references(() => users.id, { onDelete: "cascade" })
    .notNull(),
  createdAt: timestamp("created_at").defaultNow().notNull(),
});

// İlişki tanımları
export const usersRelations = relations(users, ({ many }) => ({
  posts: many(posts),
  comments: many(comments),
}));

export const postsRelations = relations(posts, ({ one, many }) => ({
  author: one(users, {
    fields: [posts.authorId],
    references: [users.id],
  }),
  comments: many(comments),
}));

export const commentsRelations = relations(comments, ({ one }) => ({
  post: one(posts, {
    fields: [comments.postId],
    references: [posts.id],
  }),
  author: one(users, {
    fields: [comments.authorId],
    references: [users.id],
  }),
}));

Bu şemada birkaç önemli detay var:

  • pgTable() fonksiyonu PostgreSQL tabloları tanımlar
  • references() ile yabancı anahtar ilişkileri kurulur
  • relations() kısmı biraz kafa karıştırıcı olabilir — veritabanında herhangi bir değişiklik yapmaz, yalnızca TypeScript seviyesinde ilişkileri tanımlar. Drizzle'ın ilişkisel sorgu API'si için gerekli.
  • Tüm tipler şemadan otomatik olarak çıkarılıyor — ayrı bir generate komutu gerekmez

Migration: Şema Değişikliklerini Veritabanına Uygulama

Drizzle Kit, şemanızdaki değişiklikleri temiz ve okunabilir SQL dosyalarına dönüştürür. Açıkçası, ürettiği SQL dosyaları Prisma'nınkilerden çok daha okunaklı. package.json dosyanıza şu betikleri ekleyin:

// package.json
{
  "scripts": {
    "db:generate": "drizzle-kit generate",
    "db:migrate": "drizzle-kit migrate",
    "db:push": "drizzle-kit push",
    "db:studio": "drizzle-kit studio"
  }
}

Migration dosyalarını oluşturup uygulamak da gayet kolay:

# Migration SQL dosyalarını oluştur
npm run db:generate

# Migration'ları veritabanına uygula
npm run db:migrate

# Alternatif: Geliştirme sırasında hızlı şema senkronizasyonu
npm run db:push

Burada küçük ama önemli bir ayrıntı var. db:push komutu, geliştirme aşamasında şemanızı migration dosyası oluşturmadan doğrudan veritabanına yansıtır — hızlı prototipleme için harika. Ama üretim ortamında kesinlikle db:generate ve db:migrate komutlarını kullanın. Şema değişikliklerinizin tam geçmişinin olması ileride canınızı kurtarır.

Oluşturulan migration dosyaları drizzle/ klasörüne kaydedilir ve sürüm kontrolüne dahil edilmelidir. Drizzle'ın ürettiği SQL dosyaları son derece temiz olduğundan, uygulamadan önce rahatlıkla inceleyebilir ve gerektiğinde düzenleyebilirsiniz.

Server Components ile Veri Çekme

İşte App Router'ın en güzel yanlarından biri burada devreye giriyor. Server Components içinde doğrudan veritabanı sorgularınızı çalıştırabiliyorsunuz. API rotası yazmaya gerek yok, "use server" direktifine bile ihtiyacınız yok — sadece sorgulayın:

// src/app/posts/page.tsx
import { db } from "@/db";
import { posts, users } from "@/db/schema";
import { desc, eq } from "drizzle-orm";
import Link from "next/link";

export default async function PostsPage() {
  // İlişkisel sorgu: Yazıları yazarlarıyla birlikte getir
  const allPosts = await db.query.posts.findMany({
    where: eq(posts.published, true),
    orderBy: desc(posts.createdAt),
    with: {
      author: true,
    },
  });

  return (
    <main>
      <h1>Blog Yazıları</h1>
      {allPosts.map((post) => (
        <article key={post.id}>
          <Link href={`/posts/${post.id}`}>
            <h2>{post.title}</h2>
          </Link>
          <p>Yazar: {post.author.name}</p>
          <time>{post.createdAt.toLocaleDateString("tr-TR")}</time>
        </article>
      ))}
    </main>
  );
}

Alternatif olarak, SQL'e daha yakın bir söz dizimi tercih ediyorsanız select API'sini kullanabilirsiniz:

// SQL benzeri select sorgusu
const publishedPosts = await db
  .select({
    id: posts.id,
    title: posts.title,
    authorName: users.name,
    createdAt: posts.createdAt,
  })
  .from(posts)
  .innerJoin(users, eq(posts.authorId, users.id))
  .where(eq(posts.published, true))
  .orderBy(desc(posts.createdAt))
  .limit(10);

İki yaklaşım da tamamen tip güvenli. Dönen veri yapısı TypeScript tarafından otomatik olarak çıkarılıyor ve IDE'nizde otomatik tamamlama desteği alıyorsunuz. Bu, geliştirme sürecini inanılmaz derecede hızlandırıyor.

Server Actions ile CRUD İşlemleri

Next.js Server Actions, form gönderimlerini ve veri mutasyonlarını sunucu tarafında işlemek için mükemmel bir mekanizma. Drizzle ORM ile bir araya geldiğinde, tip güvenli ve güvenli CRUD operasyonları oluşturmak gerçekten keyifli bir iş haline geliyor.

Zod ile Veri Doğrulama

İlk olarak drizzle-zod paketini kullanarak şemanızdan otomatik Zod doğrulama şemaları oluşturalım. Bu adım opsiyonel gibi görünse de, üretim ortamında mutlaka yapılmalı:

npm install drizzle-zod zod
// src/db/validations.ts
import { createInsertSchema, createSelectSchema } from "drizzle-zod";
import { posts } from "./schema";
import { z } from "zod";

// Şemadan otomatik doğrulama şeması oluştur
export const insertPostSchema = createInsertSchema(posts, {
  title: z.string().min(3, "Başlık en az 3 karakter olmalı").max(255),
  content: z.string().min(10, "İçerik en az 10 karakter olmalı"),
});

export const updatePostSchema = insertPostSchema
  .partial()
  .omit({ authorId: true, createdAt: true });

export type InsertPost = z.infer<typeof insertPostSchema>;
export type UpdatePost = z.infer<typeof updatePostSchema>;

Server Actions Dosyası

Şimdi asıl eğlenceli kısma geçelim — CRUD operasyonları:

// src/app/posts/actions.ts
"use server";

import { db } from "@/db";
import { posts } from "@/db/schema";
import { insertPostSchema, updatePostSchema } from "@/db/validations";
import { eq } from "drizzle-orm";
import { revalidatePath } from "next/cache";

// Yeni yazı oluştur
export async function createPost(formData: FormData) {
  const rawData = {
    title: formData.get("title") as string,
    content: formData.get("content") as string,
    authorId: Number(formData.get("authorId")),
  };

  // Zod ile doğrulama
  const validated = insertPostSchema.safeParse(rawData);

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

  const [newPost] = await db
    .insert(posts)
    .values(validated.data)
    .returning();

  revalidatePath("/posts");
  return { success: true, post: newPost };
}

// Yazı güncelle
export async function updatePost(id: number, formData: FormData) {
  const rawData = {
    title: formData.get("title") as string,
    content: formData.get("content") as string,
    published: formData.get("published") === "true",
  };

  const validated = updatePostSchema.safeParse(rawData);

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

  const [updated] = await db
    .update(posts)
    .set({ ...validated.data, updatedAt: new Date() })
    .where(eq(posts.id, id))
    .returning();

  revalidatePath("/posts");
  revalidatePath(`/posts/${id}`);
  return { success: true, post: updated };
}

// Yazı sil
export async function deletePost(id: number) {
  await db.delete(posts).where(eq(posts.id, id));

  revalidatePath("/posts");
  return { success: true };
}

İstemci Bileşeninde Kullanım

Server Actions'ı istemci tarafında kullanmak da oldukça basit. React 19'un useActionState hook'uyla form state yönetimi çocuk oyuncağı:

// src/app/posts/new/post-form.tsx
"use client";

import { createPost } from "../actions";
import { useActionState } from "react";

const initialState = { error: null, success: false };

export function PostForm() {
  const [state, formAction, isPending] = useActionState(
    async (_prev: typeof initialState, formData: FormData) => {
      return await createPost(formData);
    },
    initialState
  );

  return (
    <form action={formAction}>
      <div>
        <label htmlFor="title">Başlık</label>
        <input
          id="title"
          name="title"
          type="text"
          required
        />
        {state.error?.title && (
          <p className="text-red-500">{state.error.title}</p>
        )}
      </div>

      <div>
        <label htmlFor="content">İçerik</label>
        <textarea id="content" name="content" required />
        {state.error?.content && (
          <p className="text-red-500">{state.error.content}</p>
        )}
      </div>

      <input type="hidden" name="authorId" value="1" />

      <button type="submit" disabled={isPending}>
        {isPending ? "Kaydediliyor..." : "Yazıyı Kaydet"}
      </button>
    </form>
  );
}

İlişkisel Sorgular ve Gelişmiş Kullanım

Drizzle ORM'nin ilişkisel sorgu API'si, iç içe geçmiş verileri tek bir sorguda getirmenizi sağlıyor. Bu özellik, şemada relations() ile tanımladığınız ilişkilere dayanıyor ve gerçekten çok kullanışlı:

// Yazıyı yazar ve yorumlarla birlikte getir
const postWithDetails = await db.query.posts.findFirst({
  where: eq(posts.id, postId),
  with: {
    author: true,
    comments: {
      with: {
        author: true,
      },
      orderBy: desc(comments.createdAt),
      limit: 20,
    },
  },
});

// Belirli alanları seçerek getir
const postSummaries = await db.query.posts.findMany({
  columns: {
    id: true,
    title: true,
    createdAt: true,
  },
  with: {
    author: {
      columns: {
        name: true,
      },
    },
  },
  where: eq(posts.published, true),
});

En güzel kısmı? Bu sorgular N+1 sorgu problemini otomatik olarak çözüyor. Drizzle arka planda optimize edilmiş SQL üretiyor, siz sadece ne istediğinizi tanımlıyorsunuz.

Transaction (İşlem) Yönetimi

Birden fazla veritabanı işlemini atomik olarak gerçekleştirmeniz gerektiğinde (ki gerçek dünya uygulamalarında bu çok sık karşınıza çıkar) Drizzle'ın transaction API'sini kullanabilirsiniz:

// src/app/posts/actions.ts
export async function createPostWithComment(formData: FormData) {
  const result = await db.transaction(async (tx) => {
    // Yazıyı oluştur
    const [newPost] = await tx
      .insert(posts)
      .values({
        title: formData.get("title") as string,
        content: formData.get("content") as string,
        authorId: 1,
      })
      .returning();

    // İlk yorumu ekle
    const [firstComment] = await tx
      .insert(comments)
      .values({
        body: "İlk yorum otomatik eklendi.",
        postId: newPost.id,
        authorId: 1,
      })
      .returning();

    return { post: newPost, comment: firstComment };
  });

  revalidatePath("/posts");
  return result;
}

Transaction içindeki herhangi bir işlem başarısız olursa tüm değişiklikler otomatik olarak geri alınır. Veri tutarlılığı garanti altında.

Edge Runtime ve Sunucusuz Ortamlar

Drizzle ORM'nin sunucusuz ortamlardaki performansı gerçekten dikkat çekici. Yalnızca ~7.4 KB boyutuyla Edge Runtime'da soğuk başlatma süreleri minimum düzeyde kalıyor. Vercel Edge Functions veya Cloudflare Workers üzerinde kullanmak için Neon ya da Turso gibi sunucusuz veritabanı sürücüleriyle eşleştirmeniz yeterli:

// src/app/api/posts/route.ts
import { drizzle } from "drizzle-orm/neon-http";
import { neon } from "@neondatabase/serverless";
import * as schema from "@/db/schema";
import { eq } from "drizzle-orm";

// Edge Runtime'da çalıştır
export const runtime = "edge";

export async function GET(request: Request) {
  const sql = neon(process.env.DATABASE_URL!);
  const db = drizzle(sql, { schema });

  const allPosts = await db.query.posts.findMany({
    where: eq(schema.posts.published, true),
    with: { author: true },
  });

  return Response.json(allPosts);
}

Bu yapılandırmayla API rotanız dünya genelinde dağıtılmış edge sunucularında çalışır ve kullanıcılara en yakın noktadan yanıt verir. Özellikle global bir kullanıcı kitleniz varsa, fark gerçekten hissedilir.

Drizzle ORM ve Prisma Karşılaştırması

2026'da TypeScript ORM seçimi artık otomatik olarak "Prisma kullan" değil. Her iki aracın da güçlü ve zayıf yönleri var. İşte kısa bir karşılaştırma:

ÖzellikDrizzle ORMPrisma
Şema tanımıTypeScript-native.prisma dosyası (PSL)
Kod üretme adımıGerekli değilprisma generate gerekli
Bundle boyutu~7.4 KBÖnemli ölçüde büyük
Edge RuntimeDoğal destekPrisma Accelerate gerekli
PerformansHam SQL'e yakın (%10-20 fark)2-4x ek yük (v7 ile azaldı)
SQL yakınlığıYüksekDüşük (soyutlanmış)
Öğrenme eğrisiSQL bilgisi avantajYeni başlayanlar için daha kolay
Sunucusuz DB desteğiNeon, Turso, D1, PlanetScalePrisma Accelerate ile

Dürüst olmak gerekirse, Prisma 7 Rust sorgu motorunu kaldırarak ciddi performans iyileştirmeleri yaptı. Fakat Drizzle, edge ve sunucusuz ortamlarda hâlâ belirgin bir avantaja sahip. SQL'e yakınlık, minimal bundle boyutu ve sıfır bağımlılık felsefesi — eğer modern bir Next.js projesi üzerinde çalışıyorsanız, Drizzle'ı en azından bir değerlendirin derim.

Üretim Ortamı İçin En İyi Uygulamalar

Projenizi üretime taşımadan önce şu noktalara dikkat etmenizde fayda var:

  • Bağlantı havuzu kullanın: Sunucusuz ortamlarda bağlantı sayısını sınırlamak kritik önem taşır. Neon, Supabase veya PgBouncer gibi çözümler bu işi halleder.
  • Migration dosyalarını sürüm kontrolüne dahil edin: drizzle/ klasörünü .gitignore'a eklemeyin. Migration geçmişiniz takım genelinde paylaşılmalı.
  • Geliştirmede db:push, üretimde db:migrate kullanın: Geliştirme sırasında hızlı iterasyon için push, üretimde güvenli migration için migrate tercih edin.
  • Drizzle Studio'yu deneyin: npm run db:studio komutuyla açılan tarayıcı tabanlı veritabanı yönetim aracı, verinizi görselleştirmek ve hızlı düzenlemeler yapmak için oldukça pratik.
  • Tip dışa aktarımları oluşturun: Şemanızdan InferSelectModel ve InferInsertModel ile TypeScript tiplerini dışa aktararak uygulamanız genelinde tutarlı tipler kullanın.
// src/db/schema.ts (tip dışa aktarımları)
import type { InferSelectModel, InferInsertModel } from "drizzle-orm";

export type User = InferSelectModel<typeof users>;
export type NewUser = InferInsertModel<typeof users>;
export type Post = InferSelectModel<typeof posts>;
export type NewPost = InferInsertModel<typeof posts>;

Sıkça Sorulan Sorular

Drizzle ORM'yi mevcut bir Next.js projesine nasıl eklerim?

Mevcut bir Next.js App Router projesine Drizzle ORM eklemek oldukça basit. drizzle-orm ve veritabanı sürücünüzü yükleyin, drizzle.config.ts dosyasını oluşturun, şemanızı TypeScript ile tanımlayın ve drizzle-kit push ile veritabanınızı senkronize edin. Mevcut tablolarınız varsa drizzle-kit introspect komutuyla bunları otomatik olarak Drizzle şemasına dönüştürebilirsiniz.

Drizzle ORM Next.js Edge Runtime ile çalışır mı?

Evet, tam uyumlu. ~7.4 KB boyutuyla Vercel Edge Functions ve Cloudflare Workers'da sorunsuz çalışıyor. Neon veya Turso gibi HTTP tabanlı sunucusuz veritabanı sürücüleriyle eşleştirerek edge ortamında veritabanı sorgularını çalıştırabilirsiniz.

Drizzle ORM mi Prisma mı kullanmalıyım?

Edge veya sunucusuz ortamlara dağıtım yapıyorsanız, küçük bundle boyutu ve performans öncelikliyse ya da SQL'e yakın sorgular yazmayı seviyorsanız Drizzle'a bakın. Yeni başlıyorsanız, geniş topluluk desteği ve kapsamlı dokümantasyon istiyorsanız veya MongoDB desteğine ihtiyacınız varsa Prisma daha iyi bir seçim olabilir. 2026 itibarıyla ikisi de üretimde güvenle kullanılabilir.

Server Actions ile Drizzle ORM entegrasyonu nasıl çalışır?

Server Actions, "use server" direktifi ile tanımlanan sunucu tarafı fonksiyonlarıdır. Bu fonksiyonların içinde Drizzle ORM sorgularını doğrudan çalıştırabilirsiniz — insert, update, delete işlemleri yapıp revalidatePath() ile önbelleği güncellersiniz. Form verilerini drizzle-zod ile doğrulayarak tip güvenli bir CRUD katmanı oluşturmanız da mümkün.

Drizzle ORM ile migration yönetimi nasıl yapılır?

Drizzle Kit şemanızdaki değişiklikleri otomatik algılar ve temiz SQL migration dosyaları üretir. drizzle-kit generate komutu migration dosyasını oluşturur, drizzle-kit migrate bunu veritabanına uygular. Geliştirme sırasında hızlı gitmek istiyorsanız drizzle-kit push ile migration dosyası oluşturmadan doğrudan şemayı uygulayabilirsiniz.

Yazar Hakkında Editorial Team

Our team of expert writers and editors.