Next.js 교재 · 14편 · Route Handlers

Route Handlers — API 엔드포인트 만들기

Express 없이 같은 프로젝트 안에서 백엔드. 풀스택의 진짜 의미.

route.ts 파일이 API 엔드포인트가 되는 컨셉 일러스트

Next.js 가 "풀스택" 인 결정적 이유 — 같은 프로젝트 안에 API 엔드포인트를 만들 수 있다. Node.js 시리즈 13편에서 Express 로 별도 서버 띄우는 법을 봤다면, Next 에선 별도 서버 없이 페이지 옆에 그냥 route.ts 파일 하나 두면 끝.

이게 Route Handlers. 옛 Pages Router 시절 pages/api 가 App Router 에서 app/api/.../route.ts 로 진화했다. 사용 패턴이 완전히 달라졌으니 새로 익혀야 한다.

1. 가장 단순한 GET 엔드포인트

파일 한 개. 위치는 app/api/{경로}/route.ts.

// app/api/health/route.ts export async function GET() { return Response.json({ status: 'ok', timestamp: Date.now(), }); }

http://localhost:3000/api/health 가 즉시 생긴다. JSON 응답까지 한 함수. Response.json 은 표준 Web API (Edge·Node 양쪽 호환).

핵심 규칙 — 파일명이 route.ts여야 한다. 페이지의 page.tsx 와 같은 컨벤션. route.ts 가 있는 폴더는 페이지가 아니라 API 엔드포인트가 된다.

page.tsx 와 충돌 주의 — 같은 폴더에 page.tsxroute.ts 를 동시에 두면 Next 가 빌드 에러. 한 URL 은 페이지이거나 API 이거나 둘 중 하나. 보통 페이지는 app/blog/ 에, API 는 app/api/blog/ 에 두는 컨벤션.

2. HTTP 메서드별 함수 — GET·POST·PUT·DELETE

한 파일 안에서 메서드별로 함수를 export.

// app/api/posts/route.ts import { db } from '@/lib/db'; import { NextRequest } from 'next/server'; // GET /api/posts — 목록 export async function GET() { const posts = await db.posts.findMany(); return Response.json(posts); } // POST /api/posts — 새 글 생성 export async function POST(req: NextRequest) { const body = await req.json(); if (!body.title) { return Response.json({ error: '제목 필수' }, { status: 400 }); } const post = await db.posts.create({ data: body }); return Response.json(post, { status: 201 }); }

req.json() 한 줄로 본문 파싱 — Express 의 express.json() 미들웨어가 필요 없다. NextRequest 는 표준 Request 를 확장해 cookies·nextUrl 같은 편의 메서드 추가.

지원 메서드 — GET·POST·PUT·PATCH·DELETE·HEAD·OPTIONS. 정의 안 한 메서드로 요청 오면 자동 405 응답.

3. 동적 세그먼트 — params 받기

페이지와 같은 컨벤션. 폴더 이름에 대괄호.

// app/api/posts/[id]/route.ts import { NextRequest } from 'next/server'; type Ctx = { params: Promise<{ id: string }> }; export async function GET(req: NextRequest, ctx: Ctx) { const { id } = await ctx.params; const post = await db.posts.findUnique({ where: { id } }); if (!post) { return Response.json({ error: 'Not Found' }, { status: 404 }); } return Response.json(post); } export async function DELETE(req: NextRequest, ctx: Ctx) { const { id } = await ctx.params; await db.posts.delete({ where: { id } }); return new Response(null, { status: 204 }); }

핸들러의 두 번째 인자가 컨텍스트 객체. params 는 Next 15 부터 Promiseawait 필요. 페이지 챕터 5편과 같은 변화.

4. 쿼리·헤더·쿠키 다루기

// app/api/search/route.ts import { NextRequest } from 'next/server'; export async function GET(req: NextRequest) { // 쿼리 const { searchParams } = req.nextUrl; const q = searchParams.get('q'); const page = Number(searchParams.get('page') ?? '1'); // 헤더 const lang = req.headers.get('accept-language') ?? 'ko'; // 쿠키 (Next 확장) const token = req.cookies.get('session')?.value; // 응답에 헤더·쿠키 설정 const res = Response.json({ q, page, lang }); res.headers.set('Cache-Control', 'public, max-age=60'); return res; }

표준 Web API + Next 확장이 섞여 있다. req.nextUrl.searchParams·req.cookies 가 Next 가 더 얹은 부분. req.headers 는 Web 표준 그대로.

5. 캐싱·revalidate·에러 처리

Route Handler 의 GET 은 기본적으로 캐시 안 함 (Next 15 변경). 캐시하려면 명시적으로 지정.

// 정적 캐시 (빌드 타임) export const dynamic = 'force-static'; // 60초마다 재검증 (ISR) export const revalidate = 60; // 동적 (기본, Next 15) — 매 요청 새로 export const dynamic = 'force-dynamic';

같은 패턴이 페이지에도 적용된다. 17편 캐싱 챕터에서 깊게.

에러 처리 — 표준 패턴

export async function GET(req: NextRequest) { try { const data = await fetchExternal(); return Response.json(data); } catch (err) { console.error('[api/health]', err); return Response.json( { error: '외부 서비스 응답 없음' }, { status: 503 } ); } }

throw 를 그대로 두지 말 것 — Next 가 500 응답을 자동으로 만들어주지만 에러 내용이 클라이언트로 누출될 수 있다. 항상 try/catch + 안전한 메시지.

Route Handler vs Server Action 언제 무엇을 — 외부에서 호출되는 API(모바일 앱·웹훅·서드파티)는 Route Handler. 같은 Next 사이트 내부 폼·버튼은 Server Action(9편) 이 더 짧다. 둘 다 같은 일을 할 수 있지만, 외부 노출은 Route Handler 가 표준.

요약 — 14편 좌표

여기까지 정리. app/api/{경로}/route.ts 한 파일에 메서드별 함수(GET·POST·DELETE) export 하면 즉시 엔드포인트. 본문 파싱은 await req.json(), 쿼리는 req.nextUrl.searchParams, 응답은 Response.json(data, {status}). 동적 세그먼트는 [id] + ctx.params(Promise). 캐싱은 export const dynamic/revalidate. 외부에서 호출되는 API 는 Route Handler, 내부 폼은 Server Action. 다음 편에서 이미지·폰트 최적화로 LCP 끌어올린다.

다음 편 예고 — Image·Font 최적화

next/image·next/font 로 라이트하우스 점수 자동 상승. 15편.

© 2026 주나이테크(주) @JUNAITECH