codingstairs
노트에듀라이프연락
⌕검색⌘K
koen

Navigation

  • Intro
  • Blog
  • Life

연락하기

로그인 없이도 보낼 수 있어요. 답변이 필요하면 이메일을 함께 적어 주세요.

  • 익명 폼으로 의견 남기기 →
  • ✉ warragon112@gmail.com
  • 카카오톡 오픈채팅 ↗

© 2026 codingstairs

  • 노트
  • 에듀
  • 검색
  • 라이프
  • 연락
  • 약관
  • RSS
  • GitHub
에듀›Next.js 16 으로 첫 풀스택 앱 만들기›3단계

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 를 참고합니다.

← 2단계

2단계 — Server vs Client 컴포넌트

4단계 →

4단계 — 배포 (Vercel · Fly.io · Docker)