3단계
3단계 — API Route + Drizzle ORM
0회 조회
3단계 — API Route + Drizzle ORM
DB 연결이 풀스택의 중심. Drizzle ORM + PostgreSQL 로 타입 안전 + 가볍게.
1. PostgreSQL 준비
로컬은 Docker 가 편리.
docker run --name pg-fullstack \
-e POSTGRES_PASSWORD=postgres \
-e POSTGRES_DB=my_app \
-p 5432:5432 -d \
postgres:16-alpine
접속 확인:
docker exec -it pg-fullstack psql -U postgres -d my_app
# \q 로 종료
2. 패키지 설치
pnpm add drizzle-orm postgres
pnpm add -D drizzle-kit @types/pg
drizzle-orm— ORM 본체postgres— 네이티브 PG 드라이버 (pg-native 대비 빠름)drizzle-kit— 마이그레이션 도구
3. 환경변수
.env.local:
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/my_app
.gitignore 에 .env.local 포함.
4. 스키마 정의 — DDL
src/db/schema.ts:
import { pgTable, serial, text, timestamp, boolean } from "drizzle-orm/pg-core";
export const posts = pgTable("posts", {
id: serial("id").primaryKey(),
title: text("title").notNull(),
body: text("body").notNull(),
published: boolean("published").notNull().default(false),
created_at: timestamp("created_at").defaultNow().notNull(),
updated_at: timestamp("updated_at").defaultNow().notNull(),
});
export type Post = typeof posts.$inferSelect; // SELECT 결과 타입
export type NewPost = typeof posts.$inferInsert; // INSERT 타입
TypeScript 타입이 DDL 에서 자동 파생.
5. DB 연결 싱글톤
src/db/index.ts:
import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
import * as schema from "./schema";
const client = postgres(process.env.DATABASE_URL!, {
max: 10, // 연결 풀 상한
idle_timeout: 20,
});
export const db = drizzle(client, { schema });
max: 10 — 동시 연결 10 개. 서비스 규모에 맞게 조정.
6. 마이그레이션 설정
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! },
verbose: true,
strict: true,
});
7. 마이그레이션 생성 · 적용
# 스키마 변경 감지 → SQL 파일 생성
pnpm drizzle-kit generate
# DB 에 적용
pnpm drizzle-kit migrate
drizzle/ 폴더에 SQL 파일이 누적. Git 으로 추적.
빠른 개발 중에는 push 로 파일 없이 바로 적용:
pnpm drizzle-kit push
개발 단계만 권장. 운영에서는 generate + migrate 로 이력 보관.
8. Studio — GUI 로 확인
pnpm drizzle-kit studio
브라우저로 DB 테이블 · 데이터 확인 · 수정.
9. API Route — GET · POST
src/app/api/posts/route.ts:
import { NextResponse } from "next/server";
import { db } from "@/db";
import { posts } from "@/db/schema";
import { desc } from "drizzle-orm";
// GET /api/posts
export async function GET() {
const rows = await db
.select()
.from(posts)
.orderBy(desc(posts.created_at))
.limit(20);
return NextResponse.json(rows);
}
// POST /api/posts
export async function POST(req: Request) {
const body = await req.json();
if (!body.title || !body.body) {
return NextResponse.json({ error: "title · body 필수" }, { status: 400 });
}
const [row] = await db
.insert(posts)
.values({ title: body.title, body: body.body })
.returning();
return NextResponse.json(row, { status: 201 });
}
10. Dynamic Route
src/app/api/posts/[id]/route.ts:
import { NextResponse } from "next/server";
import { db } from "@/db";
import { posts } from "@/db/schema";
import { eq } from "drizzle-orm";
export async function GET(
_req: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params;
const [row] = await db
.select()
.from(posts)
.where(eq(posts.id, Number(id)));
if (!row) return NextResponse.json({ error: "not found" }, { status: 404 });
return NextResponse.json(row);
}
export async function DELETE(
_req: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params;
await db.delete(posts).where(eq(posts.id, Number(id)));
return NextResponse.json({ ok: true });
}
Next 15+ 부터 params 가 Promise.
11. 프론트에서 호출
// src/app/posts/PostList.tsx
"use client";
import { useEffect, useState } from "react";
type Post = { id: number; title: string; body: string };
export default function PostList() {
const [posts, setPosts] = useState<Post[]>([]);
useEffect(() => {
fetch("/api/posts").then(r => r.json()).then(setPosts);
}, []);
return (
<ul>
{posts.map(p => <li key={p.id}>{p.title}</li>)}
</ul>
);
}
Server Component 에서라면 fetch 직접 호출 대신 db.select() 가 더 간단.
12. 트랜잭션
await db.transaction(async (tx) => {
await tx.insert(posts).values({ title: "A", body: "..." });
await tx.insert(posts).values({ title: "B", body: "..." });
// 중간 에러 시 자동 ROLLBACK
});
13. 자주 걸리는 자리
DATABASE_URL없음 —.env.local확인 + 서버 재시작- 마이그레이션 충돌 — 여러 브랜치에서 생성한 SQL 파일 번호 겹침. rebase 때 수동 병합
params동기 접근 — Next 15+ 부터 async.await params- GET route 에서 mutation — POST · DELETE 로
하고픈 말
Drizzle 은 ORM 중 가장 "TS 답다" 는 평이 많습니다. db.select() 결과가 바로 타입드. 마이그레이션 · GUI · raw SQL 혼합도 깔끔.
Next
- 04-deploy
Drizzle ORM 공식 · Next.js Route Handlers 를 참고합니다.