Next.js App Router는 Pages Router의 pages/api/ 방식을 대체하는 Route Handlers를 도입했다. route.ts 파일 하나로 HTTP 메서드별 핸들러를 선언적으로 정의할 수 있으며, Web 표준 Request/Response API를 기반으로 동작한다. 기존 Express 스타일과는 달리 함수 이름이 곧 HTTP 메서드가 되는 구조라 직관적이다.
app/
└── api/
├── users/
│ ├── route.ts ← GET /api/users, POST /api/users
│ └── [id]/
│ └── route.ts ← GET /api/users/:id, PUT, DELETE
└── posts/
└── route.ts
page.tsx와 같은 디렉터리에 route.ts를 둘 수 없다. 라우트 세그먼트는 페이지 또는 핸들러 중 하나만 가진다.
// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server'
export async function GET(request: NextRequest) {
const { searchParams } = request.nextUrl
const page = Number(searchParams.get('page') ?? 1)
const users = await db.user.findMany({ skip: (page - 1) * 10, take: 10 })
return NextResponse.json({ users, page })
}
export async function POST(request: NextRequest) {
const body = await request.json()
if (!body.email) {
return NextResponse.json({ error: 'email required' }, { status: 400 })
}
const user = await db.user.create({ data: body })
return NextResponse.json(user, { status: 201 })
}
NextRequest — Web 표준 Request를 확장한 Next.js 클래스. nextUrl, cookies, geo 등의 편의 속성을 추가로 제공한다.
URL 세그먼트 값은 두 번째 인자 context의 params로 전달된다.
// app/api/users/[id]/route.ts
import { NextRequest, NextResponse } from 'next/server'
type Context = { params: Promise<{ id: string }> }
export async function GET(_req: NextRequest, { params }: Context) {
const { id } = await params
const user = await db.user.findUnique({ where: { id } })
if (!user) return NextResponse.json({ error: 'Not found' }, { status: 404 })
return NextResponse.json(user)
}
export async function DELETE(_req: NextRequest, { params }: Context) {
const { id } = await params
await db.user.delete({ where: { id } })
return new NextResponse(null, { status: 204 })
}
Next.js 15부터 params는 비동기(Promise) 타입으로 변경되었다. await params로 구조 분해해야 한다.
| 상황 | 반환 방법 |
|---|---|
| JSON 응답 | NextResponse.json(data, { status }) |
| 빈 응답 (204) | new NextResponse(null, { status: 204 }) |
| 리다이렉트 | NextResponse.redirect(new URL('/login', req.url)) |
| 스트리밍 | new Response(stream, { headers }) |
GET 핸들러는 기본적으로 정적으로 캐싱된다. request 객체를 사용하거나 dynamic 옵션을 설정하면 동적 모드로 전환된다.
// 항상 동적으로 실행
export const dynamic = 'force-dynamic'
export async function GET() {
const data = await fetch('https://api.example.com/live')
return NextResponse.json(await data.json())
}
Route Handlers는 Middleware와 조합해 인증 로직을 엣지에서 처리하거나, next/headers의 cookies·headers로 요청 컨텍스트에 접근하는 패턴으로 확장할 수 있다.