Skip to main content

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):

javascript
export default function handler(req, res) {
res.status(200).json({ message: 'Hello, Next.js API!' })
}

In the App Router (app/api/hello/route.js):

javascript
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:

json
{
"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:

javascript
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:

javascript
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 method
  • req.body: Request body (needs parsing)
  • req.query: URL query parameters
  • req.headers: Request headers
  • req.cookies: Cookies sent with the request

Response Object Methods:

  • res.status(code): Sets the status code
  • res.json(data): Sends a JSON response
  • res.send(data): Sends a response
  • res.redirect(url): Redirects to another URL
  • res.setHeader(name, value): Sets a response header

App Router

In the App Router, you use standard Web API Request and Response objects:

javascript
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:

javascript
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:

json
{
"message": "Hello, John!"
}

App Router:

javascript
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:

javascript
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:

javascript
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:

javascript
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:

javascript
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):

javascript
// 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):

javascript
// 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:

javascript
// 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:

javascript
// 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

  1. Keep handlers focused: Each API route should have a specific purpose.

  2. Use proper status codes: Send appropriate HTTP status codes for different scenarios.

  3. Validate input data: Always validate user input to prevent errors or security issues.

  4. Structure your API routes logically: Organize routes in a RESTful or logical manner.

  5. Handle errors gracefully: Provide helpful error messages without exposing sensitive information.

  6. Document your API: Consider adding comments or using tools like Swagger/OpenAPI for documentation.

  7. 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:

javascript
// __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

Exercises

  1. Create a Simple API: Build an API endpoint that returns the current time and date.

  2. CRUD Operations: Extend our task manager API to include features like task filtering and pagination.

  3. Authentication: Implement a simple authentication system with API routes for user login and registration.

  4. External API Integration: Create an API endpoint that fetches data from a public API and returns the processed result.

  5. 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! :)