Documentation
Documentation

API Routes Guide

Learn how to create and manage API routes in your Next.js application using Route Handlers.

Route Handlers

Create API endpoints using Route Handlers in the app directory:

app/api/posts/route.tstsx
1import { NextResponse } from 'next/server'
2import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs'
3import { cookies } from 'next/headers'
4
5export async function GET(request: Request) {
6 const supabase = createRouteHandlerClient({ cookies })
7 const { searchParams } = new URL(request.url)
8 const query = searchParams.get('query')
9
10 const { data, error } = await supabase
11 .from('posts')
12 .select()
13 .ilike('title', `%${query}%`)
14 .limit(10)
15
16 if (error) {
17 return NextResponse.json(
18 { error: 'Failed to fetch posts' },
19 { status: 500 }
20 )
21 }
22
23 return NextResponse.json(data)
24}
25
26export async function POST(request: Request) {
27 const supabase = createRouteHandlerClient({ cookies })
28 const json = await request.json()
29
30 const { data, error } = await supabase
31 .from('posts')
32 .insert(json)
33 .select()
34 .single()
35
36 if (error) {
37 return NextResponse.json(
38 { error: 'Failed to create post' },
39 { status: 500 }
40 )
41 }
42
43 return NextResponse.json(data, { status: 201 })
44}

API Middleware

Create middleware to handle authentication, logging, and other common functionality:

middleware.tstsx
1import { NextResponse } from 'next/server'
2import { createMiddlewareClient } from '@supabase/auth-helpers-nextjs'
3import { type NextRequest } from 'next/server'
4
5export async function middleware(request: NextRequest) {
6 const res = NextResponse.next()
7 const supabase = createMiddlewareClient({ req: request, res })
8
9 // Verify authentication
10 const {
11 data: { session },
12 } = await supabase.auth.getSession()
13
14 // Protected API routes pattern
15 if (request.nextUrl.pathname.startsWith('/api/protected')) {
16 if (!session) {
17 return NextResponse.json(
18 { error: 'Unauthorized' },
19 { status: 401 }
20 )
21 }
22
23 // Add user ID to request headers
24 const requestHeaders = new Headers(request.headers)
25 requestHeaders.set('x-user-id', session.user.id)
26
27 return NextResponse.next({
28 request: {
29 headers: requestHeaders,
30 },
31 })
32 }
33
34 return res
35}
36
37export const config = {
38 matcher: '/api/:path*',
39}

Request Validation

Validate API requests using Zod schemas:

app/api/posts/route.tstsx
1import { NextResponse } from 'next/server'
2import { z } from 'zod'
3
4const postSchema = z.object({
5 title: z.string().min(1).max(100),
6 content: z.string().min(1),
7 published: z.boolean().default(false),
8 authorId: z.string().uuid(),
9})
10
11export async function POST(request: Request) {
12 try {
13 const json = await request.json()
14 const body = postSchema.parse(json)
15
16 // Your logic here
17 const post = await createPost(body)
18
19 return NextResponse.json(post, { status: 201 })
20 } catch (error) {
21 if (error instanceof z.ZodError) {
22 return NextResponse.json(
23 { error: 'Invalid request data', details: error.errors },
24 { status: 400 }
25 )
26 }
27
28 return NextResponse.json(
29 { error: 'Internal server error' },
30 { status: 500 }
31 )
32 }
33}

Error Handling

Implement consistent error handling across your API routes:

lib/api-error.tstsx
1// Custom error classes
2export class APIError extends Error {
3 constructor(
4 message: string,
5 public status: number,
6 public code?: string
7 ) {
8 super(message)
9 this.name = 'APIError'
10 }
11}
12
13export class ValidationError extends APIError {
14 constructor(message: string) {
15 super(message, 400, 'VALIDATION_ERROR')
16 }
17}
18
19export class AuthorizationError extends APIError {
20 constructor(message: string = 'Unauthorized') {
21 super(message, 401, 'UNAUTHORIZED')
22 }
23}
24
25// Error handler utility
26export function handleError(error: unknown) {
27 if (error instanceof APIError) {
28 return NextResponse.json(
29 {
30 error: error.message,
31 code: error.code,
32 },
33 { status: error.status }
34 )
35 }
36
37 console.error('Unhandled error:', error)
38 return NextResponse.json(
39 {
40 error: 'Internal server error',
41 code: 'INTERNAL_SERVER_ERROR',
42 },
43 { status: 500 }
44 )
45}
46
47// Usage in route handler
48export async function GET() {
49 try {
50 const user = await getCurrentUser()
51 if (!user) {
52 throw new AuthorizationError()
53 }
54
55 if (!user.isAdmin) {
56 throw new APIError('Forbidden', 403, 'FORBIDDEN')
57 }
58
59 const data = await fetchData()
60 return NextResponse.json(data)
61 } catch (error) {
62 return handleError(error)
63 }
64}

CORS Configuration

Configure CORS (Cross-Origin Resource Sharing) for your API routes:

app/api/cors-example/route.tstsx
1import { type NextRequest } from 'next/server'
2
3const allowedOrigins = [
4 'https://your-app.com',
5 'https://staging.your-app.com',
6]
7
8const corsHeaders = {
9 'Access-Control-Allow-Origin': '*',
10 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
11 'Access-Control-Allow-Headers': 'Content-Type, Authorization',
12}
13
14export async function OPTIONS(request: NextRequest) {
15 const origin = request.headers.get('origin') ?? ''
16
17 if (allowedOrigins.includes(origin)) {
18 return new Response(null, {
19 status: 204,
20 headers: {
21 ...corsHeaders,
22 'Access-Control-Allow-Origin': origin,
23 },
24 })
25 }
26
27 return new Response(null, { status: 204 })
28}
29
30export async function GET(request: NextRequest) {
31 const origin = request.headers.get('origin') ?? ''
32
33 // Your route logic here
34 const data = { message: 'Hello, World!' }
35
36 return new Response(JSON.stringify(data), {
37 headers: {
38 'Content-Type': 'application/json',
39 ...(allowedOrigins.includes(origin) && {
40 'Access-Control-Allow-Origin': origin,
41 }),
42 },
43 })
44}

Rate Limiting

Implement rate limiting to protect your API from abuse:

lib/rate-limit.tstsx
1import { Redis } from '@upstash/redis'
2import { Ratelimit } from '@upstash/ratelimit'
3import { headers } from 'next/headers'
4
5const redis = Redis.fromEnv()
6
7const ratelimit = new Ratelimit({
8 redis,
9 limiter: Ratelimit.slidingWindow(10, '10 s'),
10})
11
12export async function withRateLimit(handler: Function) {
13 const ip = headers().get('x-forwarded-for') ?? '127.0.0.1'
14 const { success, limit, reset, remaining } = await ratelimit.limit(ip)
15
16 if (!success) {
17 return Response.json(
18 {
19 error: 'Too many requests',
20 limit,
21 reset,
22 remaining,
23 },
24 {
25 status: 429,
26 headers: {
27 'X-RateLimit-Limit': limit.toString(),
28 'X-RateLimit-Remaining': remaining.toString(),
29 'X-RateLimit-Reset': reset.toString(),
30 },
31 }
32 )
33 }
34
35 return handler()
36}
37
38// Usage in route handler
39export async function GET(request: Request) {
40 return withRateLimit(async () => {
41 const data = await fetchData()
42 return Response.json(data)
43 })
44}