Skip to main content

Next.js Route Handlers

In modern web development, creating seamless connections between your frontend application and backend services is essential. Next.js Route Handlers provide an elegant solution for building API endpoints directly within your Next.js application, enabling you to handle HTTP requests, interact with databases, process data, and more—all without setting up a separate backend server.

What are Route Handlers?

Route Handlers are a Next.js feature that allows you to create API endpoints within your application. They are the App Router's equivalent of API Routes in the Pages Router, but with enhanced capabilities and improved integration with React and the overall Next.js architecture.

Route Handlers work exclusively with the App Router. They allow you to create custom request handlers for a given route using the Web Request and Response APIs.

Getting Started with Route Handlers

File Convention

To create a Route Handler, you need to create a file named route.js or route.ts inside the app directory:

javascript
// app/api/hello/route.js
export async function GET() {
return new Response('Hello, Next.js!')
}

This simple example creates an API endpoint at /api/hello that responds with the text "Hello, Next.js!" when accessed with a GET request.

Supported HTTP Methods

Route Handlers support all HTTP methods:

  • GET
  • POST
  • PUT
  • PATCH
  • DELETE
  • HEAD
  • OPTIONS

Here's how you can implement different methods in the same route file:

javascript
// app/api/users/route.js
export async function GET() {
// Retrieve users
return new Response(JSON.stringify({ users: ['Alice', 'Bob', 'Charlie'] }))
}

export async function POST(request) {
// Create a new user
const body = await request.json()
// Process the body data...

return new Response(JSON.stringify({ message: 'User created' }), {
status: 201
})
}

export async function DELETE(request) {
// Delete a user
const { searchParams } = new URL(request.url)
const id = searchParams.get('id')

// Delete user with the specified ID...

return new Response(null, { status: 204 })
}

Working with Requests

Route Handlers use the standard Web Request and Response API, making them easy to work with and compatible with various web standards.

Reading Request Body

javascript
// app/api/submit/route.js
export async function POST(request) {
const data = await request.json()

// Process the data
console.log('Received data:', data)

return new Response(JSON.stringify({ success: true }), {
headers: { 'Content-Type': 'application/json' }
})
}

URL Query Parameters

You can access URL query parameters using the URL object:

javascript
// app/api/search/route.js
export async function GET(request) {
const { searchParams } = new URL(request.url)
const query = searchParams.get('query')
const limit = searchParams.get('limit') || 10

// Perform search based on query and limit
const results = await performSearch(query, Number(limit))

return new Response(JSON.stringify({ results }), {
headers: { 'Content-Type': 'application/json' }
})
}

Request Headers

You can access request headers as follows:

javascript
// app/api/protected/route.js
export async function GET(request) {
const authHeader = request.headers.get('authorization')

if (!authHeader || !authHeader.startsWith('Bearer ')) {
return new Response('Unauthorized', { status: 401 })
}

const token = authHeader.split(' ')[1]
// Validate the token...

return new Response(JSON.stringify({ protected: 'data' }), {
headers: { 'Content-Type': 'application/json' }
})
}

Response Configuration

Route Handlers offer flexible ways to configure responses:

Basic Text Response

javascript
export async function GET() {
return new Response('This is a plain text response')
}

JSON Response

javascript
export async function GET() {
const data = { message: 'This is JSON data', timestamp: new Date().toISOString() }

return new Response(JSON.stringify(data), {
headers: { 'Content-Type': 'application/json' }
})
}

NextResponse Utility

Next.js provides a handy NextResponse utility that simplifies creating responses:

javascript
import { NextResponse } from 'next/server'

export async function GET() {
return NextResponse.json({ message: 'Hello', success: true })
}

Setting Status Codes and Headers

javascript
import { NextResponse } from 'next/server'

export async function POST(request) {
try {
const data = await request.json()
// Process data...

return NextResponse.json(
{ message: 'Success' },
{ status: 201, headers: { 'Set-Cookie': 'my-cookie=value' } }
)
} catch (error) {
return NextResponse.json(
{ error: 'Failed to process request' },
{ status: 400 }
)
}
}

Dynamic Route Segments

Route Handlers work seamlessly with dynamic routes:

javascript
// app/api/products/[id]/route.js
export async function GET(request, { params }) {
const { id } = params

// Fetch the product with the given ID
const product = await fetchProductById(id)

if (!product) {
return new Response('Product not found', { status: 404 })
}

return new Response(JSON.stringify(product), {
headers: { 'Content-Type': 'application/json' }
})
}

This creates endpoints like /api/products/1, /api/products/abc, etc., where the id parameter is accessible in your handler.

Practical Examples

Let's explore some real-world applications of Route Handlers:

Authentication Endpoint

javascript
// app/api/auth/login/route.js
import { sign } from 'jsonwebtoken'
import { NextResponse } from 'next/server'

export async function POST(request) {
try {
const { email, password } = await request.json()

// Validate credentials (in a real app, check against a database)
if (email === '[email protected]' && password === 'password123') {
// Create a JWT token
const token = sign({ email }, process.env.JWT_SECRET, { expiresIn: '1h' })

return NextResponse.json(
{ success: true, token },
{
headers: {
'Set-Cookie': `token=${token}; Path=/; HttpOnly; Max-Age=3600`,
},
}
)
} else {
return NextResponse.json(
{ success: false, message: 'Invalid credentials' },
{ status: 401 }
)
}
} catch (error) {
return NextResponse.json(
{ success: false, message: 'Server error' },
{ status: 500 }
)
}
}

Database Integration

javascript
// app/api/posts/route.js
import { prisma } from '@/lib/prisma' // Assuming you have Prisma set up
import { NextResponse } from 'next/server'

export async function GET(request) {
const { searchParams } = new URL(request.url)
const page = parseInt(searchParams.get('page') || '1')
const limit = parseInt(searchParams.get('limit') || '10')

try {
const posts = await prisma.post.findMany({
skip: (page - 1) * limit,
take: limit,
include: {
author: {
select: {
name: true,
email: true,
},
},
},
})

const totalPosts = await prisma.post.count()

return NextResponse.json({
posts,
pagination: {
page,
limit,
total: totalPosts,
totalPages: Math.ceil(totalPosts / limit),
},
})
} catch (error) {
console.error('Failed to fetch posts:', error)
return NextResponse.json(
{ error: 'Failed to fetch posts' },
{ status: 500 }
)
}
}

External API Integration

javascript
// app/api/weather/route.js
import { NextResponse } from 'next/server'

export async function GET(request) {
const { searchParams } = new URL(request.url)
const city = searchParams.get('city')

if (!city) {
return NextResponse.json(
{ error: 'City parameter is required' },
{ status: 400 }
)
}

try {
const apiKey = process.env.WEATHER_API_KEY
const url = `https://api.weatherapi.com/v1/current.json?key=${apiKey}&q=${encodeURIComponent(city)}`

const response = await fetch(url)
const data = await response.json()

if (response.ok) {
return NextResponse.json(data)
} else {
return NextResponse.json(
{ error: data.error?.message || 'Failed to fetch weather data' },
{ status: response.status }
)
}
} catch (error) {
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
)
}
}

File Upload Handler

javascript
// app/api/upload/route.js
import { NextResponse } from 'next/server'
import { writeFile } from 'fs/promises'
import { join } from 'path'

export async function POST(request) {
try {
const formData = await request.formData()
const file = formData.get('file')

if (!file) {
return NextResponse.json(
{ error: 'No file provided' },
{ status: 400 }
)
}

const bytes = await file.arrayBuffer()
const buffer = Buffer.from(bytes)

// Save the file to the server
const filename = `${Date.now()}-${file.name}`
const path = join(process.cwd(), 'public/uploads', filename)

await writeFile(path, buffer)

return NextResponse.json({
success: true,
filename,
url: `/uploads/${filename}`
})
} catch (error) {
console.error('Error uploading file:', error)
return NextResponse.json(
{ error: 'Failed to upload file' },
{ status: 500 }
)
}
}

Edge Runtime

Next.js supports running Route Handlers on the Edge runtime for low-latency, globally distributed API endpoints:

javascript
// app/api/edge/route.js
export const runtime = 'edge'

export async function GET() {
return new Response(
JSON.stringify({
message: 'This is running on the Edge!',
timestamp: new Date().toISOString(),
}),
{
headers: { 'Content-Type': 'application/json' }
}
)
}

CORS Configuration

To handle Cross-Origin Resource Sharing (CORS):

javascript
// app/api/cors-enabled/route.js
export async function OPTIONS(request) {
return new Response(null, {
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
},
})
}

export async function GET() {
return new Response(JSON.stringify({ message: 'CORS-enabled endpoint' }), {
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
})
}

Common Use Cases

Route Handlers are perfect for:

  1. Building API endpoints for your Next.js application
  2. Server-side data processing
  3. Authentication and authorization services
  4. Proxy requests to third-party APIs
  5. Handling form submissions
  6. Managing file uploads
  7. Creating webhooks for external services

Summary

Next.js Route Handlers provide a powerful and flexible way to build API endpoints directly within your Next.js application. By leveraging standard Web APIs and seamlessly integrating with Next.js features, they make it easy to build full-stack applications without the need for a separate backend service.

Key takeaways:

  • Route Handlers use the route.js or route.ts file convention
  • They support all standard HTTP methods
  • They work with the standard Web Request and Response APIs
  • They can be used with dynamic routes for flexible API design
  • They offer different runtime options, including Edge for global distribution

With this foundation, you can create sophisticated backend functionality directly within your Next.js application, making your development workflow more efficient and your application architecture more cohesive.

Additional Resources

Exercises

  1. Create a route handler that returns the current time in different timezones based on a query parameter.
  2. Build a simple todo API with endpoints to create, read, update, and delete tasks.
  3. Implement a route handler that fetches data from an external API and transforms it before returning it to the client.
  4. Create a protected route that verifies a JWT token from the authorization header before allowing access.
  5. Build a file upload handler that processes image files, resizes them, and stores them in a specific directory.


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)