route.ts
: Route Handler
Next.js의 Route Handler는 API 엔드포인트를 정의하기 위한 구조이다. app/
디렉토리 내부에서 route.ts
또는 route.js
파일로 작성하며, 경로는 해당 파일이 위치한 디렉토리 구조에 따라 결정된다.
Page Route에서는 api
폴더 내에서 정의해야 했으나, Next.js 15의 App Router에서는 pages/api
대신 app/api/**/route.ts
파일을 사용한다. 해당 경로에 route.ts 파일이 있다면 해당 경로는 서버의 API 엔드 포인트로 작동한다. (단, route.ts와 page.ts가 같은 경로에 있을 수는 없다.)
Route Handler의 규칙 (Convention)
Next.js에서 Route Handler는 app/
디렉토리 내부에 위치한 route.ts
또는 route.js
파일로 정의한다. 이 파일은 API 엔드포인트를 구성하며, 위치한 경로에 따라 URL이 결정된다.
예시:
app/api/route.ts → /api
Route Handler는 page.tsx
, layout.tsx
와 마찬가지로 app/
내부의 어느 위치에나 중첩(nested)해서 작성할 수 있다. 단, 동일한 경로 레벨에 page.tsx
와 route.ts
를 동시에 작성하는 것은 허용되지 않는다.
지원되는 HTTP 메서드
Route Handler에서는 다음과 같은 HTTP 메서드가 지원된다:
GET
POST
PUT
PATCH
DELETE
HEAD
OPTIONS
각 메서드는 별도의 함수로 export하며, 요청에 따라 해당 함수가 실행된다. 예시:
// app/api/route.ts
export async function GET(request: Request) {
return new Response("GET 요청 처리 결과");
}
export async function POST(request: Request) {
const body = await request.json();
return new Response(`POST로 받은 데이터: ${body.name}`);
}
지원되지 않는 HTTP 메서드로 요청할 경우, Next.js는 자동으로 405 Method Not Allowed
응답을 반환한다.
NextRequest와 NextResponse
기본적으로 Route Handler는 Web API의 표준 객체인 Request
와 Response
를 사용한다. 여기에 더해, Next.js는 이 객체들을 확장한 NextRequest
와 NextResponse
를 제공한다.
NextRequest
NextRequest
는 Request
객체에 추가적인 기능을 제공한다. 대표적으로 다음과 같은 속성이 있다:
request.cookies.get()
request.headers.get()
request.nextUrl
→ URL 객체 (pathname, searchParams 등 접근 가능)
NextResponse
NextResponse
는 Response
객체에 응답 관련 도우미 기능을 확장한 형태이다:
NextResponse.redirect(url)
NextResponse.json(data)
NextResponse.rewrite(url)
NextResponse.next()
(middleware에서 사용)
이 확장 API를 통해 요청을 더 세밀하게 제어하거나, 미들웨어 및 캐싱과 함께 사용할 수 있다.
Route Handler의 작동 방식 (Behavior)
캐싱(Caching)
Route Handler는 기본적으로 캐시되지 않는다. 단, GET
메서드에 한해서 명시적으로 캐싱을 설정할 수 있다. 이를 위해 export const dynamic = 'force-static'
또는 export const revalidate = <초 단위>
와 같은 설정을 사용한다.
// app/items/route.ts
export const dynamic = 'force-static';
export async function GET() {
const res = await fetch('https://data.mongodb-api.com/...', {
headers: {
'Content-Type': 'application/json',
'API-Key': process.env.DATA_API_KEY,
},
});
const data = await res.json();
return Response.json({ data });
}
GET
외의 메서드 (POST
,PUT
, 등)는 어떤 설정을 하더라도 캐시되지 않는다.- 동일한 파일에
GET
,POST
가 함께 있어도POST
는 캐시 대상이 아니다.
특별한 Route Handler들
다음과 같은 메타데이터 목적의 Route Handler들은 기본적으로 정적(static)이다. 하지만 내부에서 동적 API 호출을 사용하거나 dynamic = 'force-dynamic'
등을 선언하면 동적으로 처리된다:
sitemap.ts
(사이트맵 XML)opengraph-image.tsx
(OG 이미지)icon.tsx
(favicon 등)
이들은 페이지 라우팅과는 별도로 동작하며, 정적인 리소스를 생성하는 데 사용된다.
Route Resolution (라우트 해석 규칙)
- Route Handler는 라우팅의 가장 하위 수준에서 동작하는 단위이며,
layout.tsx
,loading.tsx
,error.tsx
등과는 연결되지 않는다. - 클라이언트 측 내비게이션에도 포함되지 않는다.
- 동일 경로에
page.tsx
와route.ts
를 함께 둘 수 없다. 이 경우 충돌이 발생한다.
경로 구조 | 설명 |
---|---|
app/page.tsx + app/route.ts | 충돌 (Conflict) |
app/page.tsx + app/api/route.ts | 유효 (Valid) |
app/[user]/page.tsx + app/api/route.ts | 유효 (Valid) |
- 각
route.ts
또는page.tsx
파일은 해당 경로에서의 모든 HTTP 메서드를 처리하게 된다.
예시:
// app/page.tsx
export default function Page() {
return <h1>Hello, Next.js!</h1>;
}
// 같은 경로에서 route.ts 작성 시 충돌 발생
export async function POST(request: Request) {}
예제
Revalidate: ISR을 통한 캐시 데이터 갱신
GET
요청에 대해 revalidate
값을 설정하면 해당 경로는 Incremental Static Regeneration을 사용하여 일정 시간마다 데이터를 갱신한다.
// app/posts/route.ts
export const revalidate = 60
export async function GET() {
const data = await fetch('https://api.vercel.app/blog')
const posts = await data.json()
return Response.json(posts)
}
Cookies: 쿠키 읽기 및 설정
next/headers
의 cookies()
를 통해 서버 컨텍스트에서 쿠키를 읽거나 설정할 수 있다.
// app/api/route.ts
import { cookies } from 'next/headers'
export async function GET(request: Request) {
const cookieStore = cookies()
const token = cookieStore.get('token')
return new Response('Hello, Next.js!', {
status: 200,
headers: { 'Set-Cookie': `token=${token?.value}` },
})
}
또는 NextRequest
객체를 통해 클라이언트 요청으로부터 직접 쿠키를 읽을 수도 있다.
// app/api/route.ts
import { type NextRequest } from 'next/server'
export async function GET(request: NextRequest) {
const token = request.cookies.get('token')
}
Headers: 요청 헤더 읽기
next/headers
의 headers()
를 사용하면 서버 컨텍스트에서 요청 헤더를 읽을 수 있다.
// app/api/route.ts
import { headers } from 'next/headers'
export async function GET(request: Request) {
const headersList = headers()
const referer = headersList.get('referer')
return new Response('Hello, Next.js!', {
status: 200,
headers: { referer: referer ?? '' },
})
}
또는 NextRequest
의 request.headers
를 통해 직접 접근할 수 있다.
// app/api/route.ts
import { type NextRequest } from 'next/server'
export async function GET(request: NextRequest) {
const requestHeaders = new Headers(request.headers)
}
Redirects
next/navigation
에서 제공하는 redirect()
함수를 사용하면 서버에서 요청을 다른 URL로 리디렉션할 수 있다. 이 함수는 Response
객체를 직접 반환하지 않고, 즉시 리디렉션 처리를 수행한다.
// app/api/route.ts
import { redirect } from 'next/navigation'
export async function GET(request: Request) {
redirect('https://nextjs.org/')
}
Dynamic Route Segments
동적 세그먼트를 사용하면 URL 경로의 일부를 변수처럼 받아 처리할 수 있다. 이때 params
는 Promise
형태로 전달되므로 await
을 통해 구조 분해 할당해야 한다.
// app/items/[slug]/route.ts
export async function GET(
request: Request,
{ params }: { params: Promise<{ slug: string }> }
) {
const { slug } = await params
// slug는 'a', 'b', 'c' 등 경로에 따라 동적으로 할당됨
}
경로 파일 | 예시 URL | params 결과 |
---|---|---|
app/items/[slug]/route.ts | /items/a | { slug: 'a' } |
app/items/[slug]/route.ts | /items/b | { slug: 'b' } |
app/items/[slug]/route.ts | /items/c | { slug: 'c' } |
URL Query Parameters
Route Handler의 request
객체는 NextRequest
타입이며, nextUrl.searchParams
를 통해 쿼리 파라미터에 접근할 수 있다.
// app/api/search/route.ts
import { type NextRequest } from 'next/server'
export function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams
const query = searchParams.get('query')
// /api/search?query=hello → query === 'hello'
}
Streaming
AI 콘텐츠 생성과 같은 상황에서는 스트리밍 응답이 필요하다. AI SDK 등의 고수준 추상화 도구를 사용할 수 있으며, Web API만으로도 직접 스트리밍을 구현할 수 있다.
AI SDK를 사용하는 예:
// app/api/chat/route.ts
import { openai } from '@ai-sdk/openai'
import { StreamingTextResponse, streamText } from 'ai'
export async function POST(req: Request) {
const { messages } = await req.json()
const result = await streamText({
model: openai('gpt-4-turbo'),
messages,
})
return new StreamingTextResponse(result.toAIStream())
}
Web API를 직접 사용하는 예:
// app/api/route.ts
function sleep(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms))
}
const encoder = new TextEncoder()
async function* makeIterator() {
yield encoder.encode('<p>One</p>')
await sleep(200)
yield encoder.encode('<p>Two</p>')
await sleep(200)
yield encoder.encode('<p>Three</p>')
}
function iteratorToStream(iterator: AsyncGenerator<Uint8Array>) {
return new ReadableStream({
async pull(controller) {
const { value, done } = await iterator.next()
if (done) {
controller.close()
} else {
controller.enqueue(value)
}
},
})
}
export async function GET() {
const iterator = makeIterator()
const stream = iteratorToStream(iterator)
return new Response(stream)
}
Request Body
Route Handler에서는 Web API의 표준 메서드를 사용하여 요청 본문을 읽을 수 있다.
// app/items/route.ts
export async function POST(request: Request) {
const res = await request.json()
return Response.json({ res })
}
Request Body: FormData
multipart/form-data
형식의 요청은 request.formData()
메서드를 통해 처리할 수 있다. 이 메서드는 파일 업로드나 일반 입력값 전송 시 주로 사용된다.
// app/items/route.ts
export async function POST(request: Request) {
const formData = await request.formData()
const name = formData.get('name')
const email = formData.get('email')
return Response.json({ name, email })
}
FormData는 기본적으로 문자열로 처리되므로, 필요 시 zod-form-data
등의 유효성 검증 도구를 사용할 수 있다.
CORS 설정
특정 Route Handler에서 CORS 헤더를 명시적으로 설정할 수 있다. 이는 외부 도메인의 요청을 허용하고자 할 때 사용된다.
// app/api/route.ts
export async function GET(request: Request) {
return new Response('Hello, Next.js!', {
status: 200,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
},
})
}
여러 라우트에 동일한 CORS 정책을 적용하려면 middleware.ts
또는 next.config.js
에 공통 설정을 두는 것이 효율적이다.
Webhooks
Route Handler는 외부 서비스에서 전송하는 Webhook을 수신하는 데 사용할 수 있다. 일반적으로 request.text()
를 사용하여 원본 문자열을 파싱한 뒤 처리한다.
// app/api/route.ts
export async function POST(request: Request) {
try {
const text = await request.text()
// Webhook 데이터 처리
} catch (error) {
return new Response(`Webhook error: ${error.message}`, { status: 400 })
}
return new Response('Success!', { status: 200 })
}
Pages Router와는 달리 별도의 bodyParser 설정이 필요하지 않다.
Non-UI 응답
Route Handler는 UI가 아닌 콘텐츠를 응답하는 데 사용할 수 있다. 예를 들어 RSS, XML, 텍스트 등을 직접 반환할 수 있다.
// app/rss.xml/route.ts
export async function GET() {
return new Response(
`<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0">
<channel>
<title>Next.js Documentation</title>
<link>https://nextjs.org/docs</link>
<description>The React Framework for the Web</description>
</channel>
</rss>`,
{
headers: { 'Content-Type': 'text/xml' },
}
)
}
Segment Config Options
Route Handler는 페이지나 레이아웃과 동일한 세그먼트 구성 옵션(segment config options)을 사용할 수 있다.
// app/items/route.ts
export const dynamic = 'auto'
export const dynamicParams = true
export const revalidate = false
export const fetchCache = 'auto'
export const runtime = 'nodejs'
export const preferredRegion = 'auto'
dynamic
:'auto'
,'force-static'
,'force-dynamic'
중 선택revalidate
:false
또는 초 단위 숫자runtime
:'edge'
또는'nodejs'
preferredRegion
: Vercel 환경에서 리전 최적화를 위한 설정