API Routes in Next.js

Build production-ready RESTful endpoints with Route Handlers, TypeScript, and modern web standards.

Understanding Route Handlers

Route Handlers are Next.js's modern approach to creating API endpoints, replacing the legacy API Routes from the Pages Router. They live in your app directory and use the route.ts (or route.js) file convention to handle HTTP requests.

Every HTTP verb can be exported as an async function in your Route Handler, allowing you to handle different request methods within a single file.

Why Next.js for API Development?

Next.js has revolutionized web development by providing a seamless full-stack experience. With the App Router, developers can create sophisticated backend functionality without leaving the Next.js ecosystem. Our web development services help teams leverage these capabilities for production applications. This approach offers several advantages:

  • Unified codebase: Keep your API and frontend in a single project
  • TypeScript support: Full type safety from request to response
  • Modern web standards: Use native Request/Response APIs
  • Server Components integration: Call database and services directly from server components
  • Optimized performance: Built-in caching and edge capability support

For teams building modern web applications, Next.js Route Handlers provide a type-safe, standards-compliant approach to building APIs that integrate seamlessly with the rest of your application.

app/api/posts/[id]/route.ts
1import { NextRequest, NextResponse } from 'next/server'2 3export async function GET(4 request: NextRequest,5 { params }: { params: { id: string } }6) {7 const postId = params.id8 return NextResponse.json({ postId, message: `Fetching post ${postId}` })9}10 11export async function POST(request: NextRequest) {12 const data = await request.json()13 return NextResponse.json({ message: 'Creating post', data })14}15 16export async function PUT(request: NextRequest) {17 const data = await request.json()18 return NextResponse.json({ message: 'Updating post', data })19}20 21export async function DELETE(request: NextRequest) {22 return NextResponse.json({ message: 'Deleting post' })23}

HTTP Methods Supported

Route Handlers support all standard HTTP methods through exported functions. Each method is implemented as an async function that receives the request and returns a response. This pattern follows the Next.js route.js convention for creating API endpoints.

// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server';

// Handle GET requests
export async function GET(request: NextRequest) {
 return NextResponse.json({ message: 'Getting users' })
}

// Handle POST requests
export async function POST(request: NextRequest) {
 const data = await request.json()
 return NextResponse.json({ message: 'Creating user', data })
}

// Handle PUT requests
export async function PUT(request: NextRequest) {
 const data = await request.json()
 return NextResponse.json({ message: 'Updating user', data })
}

// Handle DELETE requests
export async function DELETE(request: NextRequest) {
 return NextResponse.json({ message: 'Deleting user' })
}

// Handle PATCH requests
export async function PATCH(request: NextRequest) {
 const data = await request.json()
 return NextResponse.json({ message: 'Patching user', data })
}

// Handle OPTIONS requests for CORS
export async function OPTIONS(request: NextRequest) {
 return new Response(null, {
 status: 204,
 headers: { 'Allow': 'GET, POST, PUT, DELETE, PATCH, OPTIONS' }
 })
}

This pattern allows you to handle all CRUD operations within a single file, keeping your API logic organized and co-located with the routes it serves.

NextRequest and NextResponse

Next.js extends the standard Web Request and Response APIs with NextRequest and NextResponse, providing additional functionality specifically designed for server-side API development. These types enable full TypeScript inference from request to response, catching errors at compile time rather than runtime.

import { NextRequest, NextResponse } from 'next/server'

export async function GET(request: NextRequest) {
 // Get the complete URL
 const url = request.url

 // Get request headers
 const userAgent = request.headers.get('user-agent')

 // Access cookies
 const cookieValue = request.cookies.get('my-cookie')

 // Get query parameters
 const searchParams = request.nextUrl.searchParams
 const query = searchParams.get('query')
 const page = searchParams.get('page')

 return NextResponse.json({
 url,
 userAgent,
 cookieValue: cookieValue?.value,
 query,
 page
 })
}

Query Parameters and Dynamic Segments

Next.js makes it straightforward to work with both query parameters and dynamic URL segments. The NextRequest API provides convenient access to all URL components:

// app/api/search/route.ts
import { NextRequest, NextResponse } from 'next/server'

export async function GET(request: NextRequest) {
 // For URL: /api/search?query=hello&page=1
 const searchParams = request.nextUrl.searchParams

 // Get single parameter
 const query = searchParams.get('query') // "hello"
 const page = searchParams.get('page') // "1"

 // Get all parameters as object
 const allParams = Object.fromEntries(searchParams.entries())

 // Check if parameter exists
 const hasFilter = searchParams.has('filter')

 // Get multiple values for same parameter
 // URL: /api/search?tag=news&tag=tech
 const tags = searchParams.getAll('tag') // ["news", "tech"]

 return NextResponse.json({ query, page, allParams, hasFilter, tags })
}

For dynamic route segments, use the params object to access URL parameters:

// app/api/posts/[id]/route.ts
export async function GET(
 request: NextRequest,
 { params }: { params: { id: string } }
) {
 const postId = params.id
 return NextResponse.json({ postId, message: `Fetching post ${postId}` })
}

Best Practices for Production APIs

Building enterprise-grade APIs requires attention to several key areas beyond basic implementation. Following Makerkit's API best practices, these patterns ensure your Route Handlers are secure, maintainable, and performant. Implementing robust AI automation solutions often requires secure API foundations.

Error Handling

Robust error handling ensures your API behaves predictably under all conditions. Always validate inputs, check authorization, and return appropriate status codes:

export async function GET(request: NextRequest) {
 try {
 const token = request.headers.get('authorization')
 if (!token) {
 return NextResponse.json(
 { error: 'Unauthorized' },
 { status: 401 }
 )
 }
 return NextResponse.json({ success: true })
 } catch (error) {
 console.error('API Error:', error)
 return NextResponse.json(
 { error: 'Internal server error' },
 { status: 500 }
 )
 }
}

Data Validation with Zod

Always validate incoming data before processing. Use a validation library like Zod for schema validation, which provides compile-time type safety and runtime checks:

import { z } from 'zod'

const UserSchema = z.object({
 email: z.string().email(),
 name: z.string().min(2).max(100),
 age: z.number().min(18).optional()
})

export async function POST(request: NextRequest) {
 try {
 const body = await request.json()
 const validatedData = UserSchema.parse(body)
 return NextResponse.json({ success: true, data: validatedData })
 } catch (error) {
 if (error instanceof z.ZodError) {
 return NextResponse.json(
 { error: 'Validation failed', details: error.errors },
 { status: 400 }
 )
 }
 return NextResponse.json({ error: 'Invalid request' }, { status: 400 })
 }
}

Security Best Practices

  1. Validate all inputs with Zod or similar libraries - never trust client data without validation
  2. Implement JWT tokens or session cookies for authentication
  3. Add rate limiting to prevent abuse and protect against DDoS attacks
  4. Use secure cookie attributes (httpOnly, secure, sameSite) to prevent XSS and CSRF attacks
  5. Set appropriate CORS headers for controlled cross-origin access
// Example: Rate limiting implementation
const rateLimit = new Map<string, { count: number; reset: number }>()

export async function GET(request: NextRequest) {
 const ip = request.ip || 'unknown'
 const now = Date.now()
 const windowMs = 60 * 1000 // 1 minute
 const maxRequests = 100

 const record = rateLimit.get(ip)
 if (!record || now > record.reset) {
 rateLimit.set(ip, { count: 1, reset: now + windowMs })
 } else if (record.count >= maxRequests) {
 return NextResponse.json(
 { error: 'Rate limit exceeded' },
 { status: 429 }
 )
 } else {
 record.count++
 }
 return NextResponse.json({ success: true })
}
Key Features of Route Handlers

TypeScript Support

Full type safety from request to response with NextRequest and NextResponse

Native Web APIs

Use standard Request/Response interfaces with Next.js extensions

Dynamic Routes

Handle dynamic URL segments like [id] and [category]/[slug]

Caching & Revalidation

Built-in support for cache-control headers and on-demand revalidation

Cookie & Header APIs

Dedicated utilities for reading and setting cookies and headers

Server Actions Integration

Choose between Route Handlers and Server Actions based on use case

Performance Optimization

APIs should be fast and efficient. Following performance optimization strategies, these patterns ensure your Route Handlers deliver optimal response times.

Caching Strategies

Next.js provides multiple caching mechanisms for API responses. Use cache-control headers to control how responses are stored and revalidated:

export async function GET(request: NextRequest) {
 return NextResponse.json(
 { data: 'cached-content' },
 {
 headers: {
 'Cache-Control': 'public, s-maxage=60, stale-while-revalidate=300'
 }
 }
 )
}

Parallel Data Fetching

Use Promise.all() to fetch data in parallel, reducing overall response time when fetching from multiple sources:

export async function GET(request: NextRequest) {
 const [users, posts, stats] = await Promise.all([
 getUsers(),
 getPosts(),
 getStats()
 ])
 return NextResponse.json({ users, posts, stats })
}

Streaming for Long-Running Operations

Use streaming for responses that take time to compute, providing immediate feedback to users:

export async function GET(request: NextRequest) {
 const encoder = new TextEncoder()
 const stream = new ReadableStream({
 async start(controller) {
 controller.enqueue(encoder.encode('Processing... '))
 for (let i = 1; i <= 10; i++) {
 await new Promise(resolve => setTimeout(resolve, 500))
 controller.enqueue(encoder.encode(`Step ${i}/10 completed. `))
 }
 controller.enqueue(encoder.encode('Done!'))
 controller.close()
 }
 })
 return new NextResponse(stream, {
 headers: { 'Content-Type': 'text/plain' }
 })
}

Server Actions vs Route Handlers

Next.js offers two primary approaches for server-side logic: Route Handlers and Server Actions. Understanding when to use each is crucial for building maintainable applications. When building AI-powered solutions, choosing the right approach ensures optimal performance and user experience.

Use Route Handlers when:

  • Building RESTful APIs for external clients
  • Need standard HTTP methods (GET, POST, PUT, DELETE)
  • Creating webhooks or API integrations
  • Building public APIs for third-party access

Use Server Actions when:

  • Working primarily with React components
  • Form submissions from client components
  • Mutations triggered by UI interactions
  • Progressive enhancement scenarios
// Route Handler - For external API clients
// app/api/users/route.ts
export async function POST(request: NextRequest) {
 const data = await request.json()
 const user = await createUser(data)
 return NextResponse.json(user)
}

// Server Action - For React form submissions
// app/actions.ts
'use server'

export async function createUser(formData: FormData) {
 const data = {
 name: formData.get('name'),
 email: formData.get('email')
 }
 await db.user.create({ data })
 return { success: true }
}

Migrating from Pages Router

If you're upgrading from the Pages Router, here's how API Routes map to Route Handlers in the App Router:

Pages Router (api/)App Router (route.ts)
pages/api/users.tsapp/api/users/route.ts
pages/api/users/[id].tsapp/api/users/[id]/route.ts
res.status(200).json()return NextResponse.json()
req.cookiesrequest.cookies or cookies()
req.queryrequest.nextUrl.searchParams
// Old Pages Router style
// pages/api/users.ts
export default function handler(req, res) {
 if (req.method === 'POST') {
 const data = req.body
 res.status(200).json({ message: 'Created', data })
 }
}

// New App Router style
// app/api/users/route.ts
export async function POST(request: NextRequest) {
 const data = await request.json()
 return NextResponse.json({ message: 'Created', data })
}

The App Router approach provides better type safety, uses modern async/await patterns, and integrates seamlessly with React Server Components.

Frequently Asked Questions

When should I use Route Handlers vs Server Actions?

Use Route Handlers for building RESTful APIs, webhooks, or when external clients need to access your endpoints. Use Server Actions for form submissions and mutations triggered by React components.

How do I handle authentication in Route Handlers?

Check authorization headers or cookies, validate tokens (JWT, session), and return 401 responses for unauthenticated requests. Use httpOnly cookies for secure session management.

Can I use Route Handlers with database ORMs?

Yes. Route Handlers work seamlessly with Prisma, Drizzle, and other ORMs. Connection pooling is typically handled automatically by the ORM.

How do I enable CORS for my API?

Set appropriate Access-Control headers on your responses, including Access-Control-Allow-Origin, Access-Control-Allow-Methods, and Access-Control-Allow-Headers. Handle OPTIONS requests for preflight checks.

Ready to Build Your API?

Our team specializes in building scalable, secure APIs with Next.js and modern web technologies. From Route Handlers to full-stack architecture, we help you create robust backend services.

Sources

  1. Next.js Documentation: route.js - Official API reference for Route Handlers
  2. Makerkit: Next.js API Routes - The Ultimate Guide - Production best practices guide
  3. Artoon Solutions: Next.js API Guide 2025 - Current year guide covering API Routes and security
  4. Next.js Blog: Building APIs with Next.js - Official blog on API development