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.tsx 와 route.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 부터 Promise 라 await 필요. 페이지 챕터 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편.