Route Handlers (API Routes)
Route Handlers let you create API endpoints in the App Router. They're defined using route.ts files and support all HTTP methods.
Basic Route Handler
Create a route.ts file to define an API endpoint:
app/
└── api/
└── hello/
└── route.ts → GET /api/hello
// app/api/hello/route.ts
export async function GET() {
return Response.json({ message: 'Hello World' })
}
HTTP Methods
Export functions named after HTTP methods:
// app/api/posts/route.ts
// GET /api/posts
export async function GET() {
const posts = await db.posts.findMany()
return Response.json(posts)
}
// POST /api/posts
export async function POST(request: Request) {
const body = await request.json()
const post = await db.posts.create({ data: body })
return Response.json(post, { status: 201 })
}
Supported methods: GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS
Accessing Request Data
export async function POST(request: Request) {
// JSON body
const body = await request.json()
// Form data
const formData = await request.formData()
const name = formData.get('name')
// URL search params
const { searchParams } = new URL(request.url)
const query = searchParams.get('q')
// Headers
const authHeader = request.headers.get('authorization')
return Response.json({ received: true })
}
Dynamic Routes
Use the same [param] convention:
app/
└── api/
└── posts/
└── [id]/
└── route.ts → /api/posts/123
// app/api/posts/[id]/route.ts
export async function GET(
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await 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(
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params
await db.posts.delete({ where: { id } })
return new Response(null, { status: 204 })
}
Response Helpers
// JSON response
return Response.json(data)
// With status code
return Response.json(data, { status: 201 })
// Custom headers
return Response.json(data, {
headers: {
'Cache-Control': 'max-age=3600',
},
})
// Redirect
return Response.redirect(new URL('/login', request.url))
// No content
return new Response(null, { status: 204 })
Caching Behavior
GET handlers are cached by default with static routes:
// Cached by default (static route)
export async function GET() {
const data = await fetch('https://api.example.com/data')
return Response.json(data)
}
// Opt out of caching
export const dynamic = 'force-dynamic'
export async function GET() {
// Always fresh
}
When to Use Route Handlers
| Use Route Handlers | Use Server Actions |
|---|---|
| External API consumption | Form submissions |
| Webhooks from third parties | Mutations with revalidation |
| Public API for your app | Internal data mutations |
| File uploads (streams) | Progressive enhancement |
CORS Configuration
Handle CORS for external access:
export async function OPTIONS() {
return new Response(null, {
status: 204,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
},
})
}
export async function GET() {
return Response.json(
{ data: 'example' },
{
headers: {
'Access-Control-Allow-Origin': '*',
},
}
)
}
Summary
- Use
route.tsfiles to create API endpoints - Export functions named after HTTP methods (GET, POST, etc.)
- Access request data via
request.json(),request.formData(), etc. - Use
Response.json()for JSON responses - Dynamic routes work the same as pages (
[id]) - GET handlers are cached by default on static routes
- Consider Server Actions for internal mutations

