Next.js API Handlers
Introduction
API handlers in Next.js allow you to create serverless functions that can be accessed via API endpoints. These handlers are a key part of Next.js's full-stack capabilities, enabling you to build backend functionality directly within your Next.js application. This means you can create APIs without setting up a separate backend server!
In this guide, we'll explore how to create API routes, handle different HTTP methods, manage requests and responses, and implement common API patterns using Next.js API handlers.
What are API Handlers?
API handlers in Next.js are JavaScript functions that are automatically deployed as serverless functions. Each file inside the pages/api
directory (or app/api
in the App Router) becomes an API endpoint that can be accessed by clients.
These handlers receive HTTP requests and send responses, allowing you to build server-side logic without managing a separate backend.
Creating Your First API Handler
Basic Structure
Let's start by creating a simple API endpoint that returns a greeting message.
In the Pages Router (pages/api/hello.js
):
export default function handler(req, res) {
res.status(200).json({ message: 'Hello, Next.js API!' })
}
In the App Router (app/api/hello/route.js
):
export async function GET() {
return new Response(JSON.stringify({ message: 'Hello, Next.js API!' }), {
status: 200,
headers: {
'Content-Type': 'application/json'
}
})
}
When you send a GET request to /api/hello
, you'll receive:
{
"message": "Hello, Next.js API!"
}
HTTP Methods in API Handlers
API handlers can respond to different HTTP methods (GET, POST, PUT, DELETE, etc.).
Pages Router Approach
In the Pages Router, you can use a single function and check the request method:
export default function handler(req, res) {
const { method } = req
switch (method) {
case 'GET':
// Handle GET request
res.status(200).json({ message: 'This is a GET request' })
break
case 'POST':
// Handle POST request
res.status(201).json({ message: 'This is a POST request' })
break
default:
res.setHeader('Allow', ['GET', 'POST'])
res.status(405).end(`Method ${method} Not Allowed`)
}
}
App Router Approach
In the App Router, you define separate export functions for each HTTP method:
export async function GET() {
return new Response(JSON.stringify({ message: 'This is a GET request' }), {
status: 200,
headers: { 'Content-Type': 'application/json' }
})
}
export async function POST() {
return new Response(JSON.stringify({ message: 'This is a POST request' }), {
status: 201,
headers: { 'Content-Type': 'application/json' }
})
}
Request and Response Objects
Pages Router
In the Pages Router, the req
and res
objects provide access to request data and methods for sending responses.
Request Object Properties:
req.method
: The HTTP methodreq.body
: Request body (needs parsing)req.query
: URL query parametersreq.headers
: Request headersreq.cookies
: Cookies sent with the request
Response Object Methods:
res.status(code)
: Sets the status coderes.json(data)
: Sends a JSON responseres.send(data)
: Sends a responseres.redirect(url)
: Redirects to another URLres.setHeader(name, value)
: Sets a response header
App Router
In the App Router, you use standard Web API Request and Response objects:
export async function POST(request) {
// Get request body
const data = await request.json()
// Get query parameters
const { searchParams } = new URL(request.url)
const query = searchParams.get('query')
// Get headers
const apiKey = request.headers.get('x-api-key')
// Return response
return Response.json({ received: data, query })
}
Handling Request Data
Query Parameters
Pages Router:
export default function handler(req, res) {
const { name } = req.query
res.status(200).json({ message: `Hello, ${name || 'Guest'}!` })
}
Using this endpoint with /api/hello?name=John
would return:
{
"message": "Hello, John!"
}
App Router:
export async function GET(request) {
const { searchParams } = new URL(request.url)
const name = searchParams.get('name')
return Response.json({ message: `Hello, ${name || 'Guest'}!` })
}
Request Body
Pages Router:
export default function handler(req, res) {
if (req.method === 'POST') {
const { username, email } = req.body
// Process the data
// For example, store in a database
res.status(201).json({
message: 'User created',
user: { username, email }
})
} else {
res.status(405).json({ message: 'Method not allowed' })
}
}
App Router:
export async function POST(request) {
const data = await request.json()
const { username, email } = data
// Process the data
return new Response(JSON.stringify({
message: 'User created',
user: { username, email }
}), {
status: 201,
headers: { 'Content-Type': 'application/json' }
})
}
Error Handling in API Routes
Proper error handling is crucial for building robust APIs. Here's how to handle errors in your API handlers:
Pages Router:
export default function handler(req, res) {
try {
// Simulate an error
if (!req.query.id) {
throw new Error('ID parameter is required')
}
// Normal processing
res.status(200).json({ data: `Item ${req.query.id}` })
} catch (error) {
res.status(400).json({ error: error.message })
}
}
App Router:
export async function GET(request) {
try {
const { searchParams } = new URL(request.url)
const id = searchParams.get('id')
if (!id) {
return new Response(JSON.stringify({ error: 'ID parameter is required' }), {
status: 400,
headers: { 'Content-Type': 'application/json' }
})
}
return Response.json({ data: `Item ${id}` })
} catch (error) {
return new Response(JSON.stringify({ error: error.message }), {
status: 500,
headers: { 'Content-Type': 'application/json' }
})
}
}
Practical Example: Building a Simple CRUD API
Let's build a simple API for managing a list of tasks.
Pages Router (pages/api/tasks/index.js
):
// Mock database
let tasks = [
{ id: '1', title: 'Learn Next.js', completed: false },
{ id: '2', title: 'Build a project', completed: false }
]
export default function handler(req, res) {
const { method } = req
switch (method) {
case 'GET':
// Get all tasks
res.status(200).json(tasks)
break
case 'POST':
// Create a new task
const { title } = req.body
if (!title) {
return res.status(400).json({ error: 'Title is required' })
}
const newTask = {
id: Date.now().toString(),
title,
completed: false
}
tasks.push(newTask)
res.status(201).json(newTask)
break
default:
res.setHeader('Allow', ['GET', 'POST'])
res.status(405).end(`Method ${method} Not Allowed`)
}
}
And for individual task operations (pages/api/tasks/[id].js
):
// Reference to our mock database
let tasks = [
{ id: '1', title: 'Learn Next.js', completed: false },
{ id: '2', title: 'Build a project', completed: false }
]
export default function handler(req, res) {
const {
query: { id },
method
} = req
// Find task by ID
const taskIndex = tasks.findIndex(task => task.id === id)
if (taskIndex === -1) {
return res.status(404).json({ error: 'Task not found' })
}
switch (method) {
case 'GET':
// Get a specific task
res.status(200).json(tasks[taskIndex])
break
case 'PUT':
// Update a task
const updatedTask = {
...tasks[taskIndex],
...req.body
}
tasks[taskIndex] = updatedTask
res.status(200).json(updatedTask)
break
case 'DELETE':
// Delete a task
const deletedTask = tasks[taskIndex]
tasks = tasks.filter(task => task.id !== id)
res.status(200).json(deletedTask)
break
default:
res.setHeader('Allow', ['GET', 'PUT', 'DELETE'])
res.status(405).end(`Method ${method} Not Allowed`)
}
}
API Middleware
You can create middleware functions to handle common operations like authentication, logging, or error handling.
Pages Router Example:
// middleware/withAuth.js
export function withAuth(handler) {
return (req, res) => {
const token = req.headers.authorization
if (!token || token !== 'Bearer valid-token') {
return res.status(401).json({ error: 'Unauthorized' })
}
return handler(req, res)
}
}
// pages/api/protected-route.js
import { withAuth } from '../../middleware/withAuth'
function handler(req, res) {
res.status(200).json({ message: 'This is protected data' })
}
export default withAuth(handler)
App Router Example:
In the App Router, you would typically use middleware files directly:
// middleware.js (root of your project)
export function middleware(request) {
const token = request.headers.get('authorization')
// Check if this is an API route
if (request.nextUrl.pathname.startsWith('/api/protected')) {
if (!token || token !== 'Bearer valid-token') {
return new Response(JSON.stringify({ error: 'Unauthorized' }), {
status: 401,
headers: { 'Content-Type': 'application/json' }
})
}
}
}
export const config = {
matcher: '/api/protected/:path*',
}
Best Practices for API Handlers
-
Keep handlers focused: Each API route should have a specific purpose.
-
Use proper status codes: Send appropriate HTTP status codes for different scenarios.
-
Validate input data: Always validate user input to prevent errors or security issues.
-
Structure your API routes logically: Organize routes in a RESTful or logical manner.
-
Handle errors gracefully: Provide helpful error messages without exposing sensitive information.
-
Document your API: Consider adding comments or using tools like Swagger/OpenAPI for documentation.
-
Rate limiting: Implement rate limiting for production APIs to prevent abuse.
API Testing
You can test your API endpoints using tools like Postman, Insomnia, or by writing tests.
Here's an example of testing an API endpoint with Jest and fetch
:
// __tests__/api/hello.test.js
import { createServer } from 'http'
import { apiResolver } from 'next/dist/server/api-utils/node'
import handler from '../../pages/api/hello'
describe('Hello API', () => {
test('returns a greeting', async () => {
const mockReq = {
method: 'GET',
headers: {}
}
const mockRes = {
setHeader: jest.fn(),
status: jest.fn().mockReturnThis(),
json: jest.fn(),
end: jest.fn()
}
await handler(mockReq, mockRes)
expect(mockRes.status).toHaveBeenCalledWith(200)
expect(mockRes.json).toHaveBeenCalledWith({ message: 'Hello, Next.js API!' })
})
})
Summary
Next.js API handlers provide a powerful way to build server-side functionality directly within your Next.js application. They allow you to:
- Create API endpoints with minimal setup
- Handle different HTTP methods
- Process request data from body, query parameters, and headers
- Send formatted responses
- Implement middleware for cross-cutting concerns
Whether you're building a simple backend for your frontend application or creating a full-fledged API, Next.js API handlers offer the tools you need to build robust server-side functionality.
Additional Resources
- Next.js API Routes Documentation
- Next.js Route Handlers (App Router)
- RESTful API Design Best Practices
- HTTP Status Codes
Exercises
-
Create a Simple API: Build an API endpoint that returns the current time and date.
-
CRUD Operations: Extend our task manager API to include features like task filtering and pagination.
-
Authentication: Implement a simple authentication system with API routes for user login and registration.
-
External API Integration: Create an API endpoint that fetches data from a public API and returns the processed result.
-
API Documentation: Document your API using Swagger/OpenAPI and integrate it with your Next.js application.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)