Chapter 8: API Routes and Backend Functionality
What are API Routes in Next.js?
API Routes allow you to create backend functionality within your Next.js application. They are serverless functions that handle HTTP requests and can be used to build REST APIs, handle form submissions, integrate with databases, and more.
Key Features:
- Serverless Functions: Each API route is a serverless function
- Built-in Routing: File-based API routing in
pages/api/
- Request/Response Handling: Full access to request and response objects
- Middleware Support: Built-in middleware for authentication, CORS, etc.
- TypeScript Support: Full TypeScript support out of the box
- Edge Runtime: Optional edge runtime for better performance
Why Use API Routes?
Benefits:
- Full-Stack Development: Build frontend and backend in one project
- Serverless: Automatic scaling and deployment
- No Server Management: No need to manage separate backend servers
- Type Safety: Share types between frontend and backend
- Deployment Simplicity: Deploy everything together
- Cost Effective: Pay only for what you use
How to Create API Routes
1. Basic API Route Structure
Simple GET Endpoint:
// pages/api/hello.js
export default function handler(req, res) {
res.status(200).json({ message: 'Hello from Next.js API!' })
}
HTTP Method Handling:
// pages/api/users.js
export default function handler(req, res) {
const { method } = req
switch (method) {
case 'GET':
return handleGet(req, res)
case 'POST':
return handlePost(req, res)
case 'PUT':
return handlePut(req, res)
case 'DELETE':
return handleDelete(req, res)
default:
res.setHeader('Allow', ['GET', 'POST', 'PUT', 'DELETE'])
res.status(405).end(`Method ${method} Not Allowed`)
}
}
async function handleGet(req, res) {
try {
const users = await fetchUsers()
res.status(200).json(users)
} catch (error) {
res.status(500).json({ error: 'Failed to fetch users' })
}
}
async function handlePost(req, res) {
try {
const user = await createUser(req.body)
res.status(201).json(user)
} catch (error) {
res.status(400).json({ error: 'Failed to create user' })
}
}
async function handlePut(req, res) {
try {
const user = await updateUser(req.body)
res.status(200).json(user)
} catch (error) {
res.status(400).json({ error: 'Failed to update user' })
}
}
async function handleDelete(req, res) {
try {
await deleteUser(req.body.id)
res.status(204).end()
} catch (error) {
res.status(400).json({ error: 'Failed to delete user' })
}
}
2. Dynamic API Routes
Single Dynamic Parameter:
// pages/api/users/[id].js
export default async function handler(req, res) {
const { id } = req.query
const { method } = req
switch (method) {
case 'GET':
try {
const user = await getUserById(id)
if (!user) {
return res.status(404).json({ error: 'User not found' })
}
res.status(200).json(user)
} catch (error) {
res.status(500).json({ error: 'Failed to fetch user' })
}
break
case 'PUT':
try {
const updatedUser = await updateUser(id, req.body)
res.status(200).json(updatedUser)
} catch (error) {
res.status(400).json({ error: 'Failed to update user' })
}
break
case 'DELETE':
try {
await deleteUser(id)
res.status(204).end()
} catch (error) {
res.status(400).json({ error: 'Failed to delete user' })
}
break
default:
res.setHeader('Allow', ['GET', 'PUT', 'DELETE'])
res.status(405).end(`Method ${method} Not Allowed`)
}
}
Multiple Dynamic Parameters:
// pages/api/posts/[userId]/[postId].js
export default async function handler(req, res) {
const { userId, postId } = req.query
const { method } = req
switch (method) {
case 'GET':
try {
const post = await getPostByUserAndId(userId, postId)
if (!post) {
return res.status(404).json({ error: 'Post not found' })
}
res.status(200).json(post)
} catch (error) {
res.status(500).json({ error: 'Failed to fetch post' })
}
break
default:
res.setHeader('Allow', ['GET'])
res.status(405).end(`Method ${method} Not Allowed`)
}
}
Catch-all Routes:
// pages/api/docs/[...slug].js
export default function handler(req, res) {
const { slug } = req.query
const path = slug.join('/')
// Handle different documentation paths
switch (path) {
case 'api/users':
return res.status(200).json({ docs: 'User API documentation' })
case 'api/posts':
return res.status(200).json({ docs: 'Post API documentation' })
default:
return res.status(404).json({ error: 'Documentation not found' })
}
}
3. Request and Response Handling
Reading Request Data:
// pages/api/contact.js
export default async function handler(req, res) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' })
}
try {
const { name, email, message } = req.body
// Validate required fields
if (!name || !email || !message) {
return res.status(400).json({
error: 'Missing required fields',
required: ['name', 'email', 'message']
})
}
// Validate email format
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!emailRegex.test(email)) {
return res.status(400).json({ error: 'Invalid email format' })
}
// Process the contact form
await sendContactEmail({ name, email, message })
res.status(200).json({
message: 'Contact form submitted successfully',
id: generateId()
})
} catch (error) {
console.error('Contact form error:', error)
res.status(500).json({ error: 'Failed to submit contact form' })
}
}
Setting Response Headers:
// pages/api/data.js
export default function handler(req, res) {
// Set CORS headers
res.setHeader('Access-Control-Allow-Origin', '*')
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE')
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization')
// Set cache headers
res.setHeader('Cache-Control', 'public, s-maxage=60, stale-while-revalidate=300')
// Set content type
res.setHeader('Content-Type', 'application/json')
const data = { message: 'Hello from API' }
res.status(200).json(data)
}
File Upload Handling:
// pages/api/upload.js
import formidable from 'formidable'
import fs from 'fs'
export const config = {
api: {
bodyParser: false,
},
}
export default async function handler(req, res) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' })
}
try {
const form = formidable({
uploadDir: './public/uploads',
keepExtensions: true,
})
const [fields, files] = await form.parse(req)
const file = files.file[0]
const { name, email } = fields
// Process the uploaded file
const fileInfo = {
originalName: file.originalFilename,
fileName: file.newFilename,
size: file.size,
type: file.mimetype,
uploadedBy: { name: name[0], email: email[0] },
uploadedAt: new Date().toISOString(),
}
res.status(200).json({
message: 'File uploaded successfully',
file: fileInfo
})
} catch (error) {
console.error('Upload error:', error)
res.status(500).json({ error: 'Failed to upload file' })
}
}
4. Authentication and Authorization
JWT Authentication:
// pages/api/auth/login.js
import jwt from 'jsonwebtoken'
import bcrypt from 'bcryptjs'
export default async function handler(req, res) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' })
}
try {
const { email, password } = req.body
// Find user in database
const user = await findUserByEmail(email)
if (!user) {
return res.status(401).json({ error: 'Invalid credentials' })
}
// Verify password
const isValidPassword = await bcrypt.compare(password, user.password)
if (!isValidPassword) {
return res.status(401).json({ error: 'Invalid credentials' })
}
// Generate JWT token
const token = jwt.sign(
{ userId: user.id, email: user.email },
process.env.JWT_SECRET,
{ expiresIn: '7d' }
)
// Set HTTP-only cookie
res.setHeader('Set-Cookie', [
`token=${token}; HttpOnly; Path=/; Max-Age=${7 * 24 * 60 * 60}; SameSite=Strict`
])
res.status(200).json({
message: 'Login successful',
user: {
id: user.id,
email: user.email,
name: user.name
}
})
} catch (error) {
console.error('Login error:', error)
res.status(500).json({ error: 'Login failed' })
}
}
Protected Route Middleware:
// lib/auth.js
import jwt from 'jsonwebtoken'
export function verifyToken(req) {
const token = req.headers.authorization?.replace('Bearer ', '') ||
req.cookies.token
if (!token) {
throw new Error('No token provided')
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET)
return decoded
} catch (error) {
throw new Error('Invalid token')
}
}
// pages/api/protected.js
import { verifyToken } from '../../lib/auth'
export default async function handler(req, res) {
try {
// Verify authentication
const user = verifyToken(req)
// Handle the protected route
const data = await getProtectedData(user.userId)
res.status(200).json({ data, user })
} catch (error) {
res.status(401).json({ error: error.message })
}
}
5. Database Integration
MongoDB Integration:
// lib/mongodb.js
import { MongoClient } from 'mongodb'
const client = new MongoClient(process.env.MONGODB_URI)
let cachedClient = null
export async function connectToDatabase() {
if (cachedClient) {
return cachedClient
}
try {
await client.connect()
cachedClient = client
return client
} catch (error) {
console.error('MongoDB connection error:', error)
throw error
}
}
// pages/api/posts.js
import { connectToDatabase } from '../../lib/mongodb'
export default async function handler(req, res) {
const { method } = req
try {
const { db } = await connectToDatabase()
switch (method) {
case 'GET':
const posts = await db.collection('posts').find({}).toArray()
res.status(200).json(posts)
break
case 'POST':
const { title, content, author } = req.body
const result = await db.collection('posts').insertOne({
title,
content,
author,
createdAt: new Date(),
})
res.status(201).json({ id: result.insertedId })
break
default:
res.setHeader('Allow', ['GET', 'POST'])
res.status(405).end(`Method ${method} Not Allowed`)
}
} catch (error) {
console.error('Database error:', error)
res.status(500).json({ error: 'Database operation failed' })
}
}
Prisma Integration:
// lib/prisma.js
import { PrismaClient } from '@prisma/client'
const globalForPrisma = globalThis
export const prisma = globalForPrisma.prisma || new PrismaClient()
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
// pages/api/users.js
import { prisma } from '../../lib/prisma'
export default async function handler(req, res) {
const { method } = req
try {
switch (method) {
case 'GET':
const users = await prisma.user.findMany({
include: { posts: true }
})
res.status(200).json(users)
break
case 'POST':
const { name, email } = req.body
const user = await prisma.user.create({
data: { name, email }
})
res.status(201).json(user)
break
default:
res.setHeader('Allow', ['GET', 'POST'])
res.status(405).end(`Method ${method} Not Allowed`)
}
} catch (error) {
console.error('Prisma error:', error)
res.status(500).json({ error: 'Database operation failed' })
}
}
6. External API Integration
Third-party API Integration:
// pages/api/weather.js
export default async function handler(req, res) {
if (req.method !== 'GET') {
return res.status(405).json({ error: 'Method not allowed' })
}
try {
const { city } = req.query
if (!city) {
return res.status(400).json({ error: 'City parameter is required' })
}
const response = await fetch(
`https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${process.env.OPENWEATHER_API_KEY}&units=metric`
)
if (!response.ok) {
throw new Error('Weather API request failed')
}
const weatherData = await response.json()
// Transform the data
const transformedData = {
city: weatherData.name,
country: weatherData.sys.country,
temperature: weatherData.main.temp,
description: weatherData.weather[0].description,
humidity: weatherData.main.humidity,
windSpeed: weatherData.wind.speed,
}
res.status(200).json(transformedData)
} catch (error) {
console.error('Weather API error:', error)
res.status(500).json({ error: 'Failed to fetch weather data' })
}
}
Webhook Handling:
// pages/api/webhooks/stripe.js
import Stripe from 'stripe'
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY)
export default async function handler(req, res) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' })
}
try {
const sig = req.headers['stripe-signature']
const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET
let event
try {
event = stripe.webhooks.constructEvent(req.body, sig, endpointSecret)
} catch (err) {
console.error('Webhook signature verification failed:', err.message)
return res.status(400).send(`Webhook Error: ${err.message}`)
}
// Handle the event
switch (event.type) {
case 'payment_intent.succeeded':
const paymentIntent = event.data.object
await handleSuccessfulPayment(paymentIntent)
break
case 'payment_intent.payment_failed':
const failedPayment = event.data.object
await handleFailedPayment(failedPayment)
break
default:
console.log(`Unhandled event type ${event.type}`)
}
res.status(200).json({ received: true })
} catch (error) {
console.error('Webhook error:', error)
res.status(500).json({ error: 'Webhook processing failed' })
}
}
Advanced API Route Patterns
1. API Route Middleware
// lib/apiMiddleware.js
export function withAuth(handler) {
return async (req, res) => {
try {
const user = verifyToken(req)
req.user = user
return handler(req, res)
} catch (error) {
return res.status(401).json({ error: 'Unauthorized' })
}
}
}
export function withValidation(schema) {
return (handler) => {
return async (req, res) => {
try {
await schema.validate(req.body)
return handler(req, res)
} catch (error) {
return res.status(400).json({ error: error.message })
}
}
}
}
// pages/api/protected.js
import { withAuth } from '../../lib/apiMiddleware'
async function handler(req, res) {
res.status(200).json({ message: 'Protected data', user: req.user })
}
export default withAuth(handler)
2. API Route Composition
// lib/apiComposition.js
export function compose(...middlewares) {
return middlewares.reduce((a, b) => (req, res) => a(req, res, () => b(req, res)))
}
// pages/api/complex.js
import { compose, withAuth, withValidation, withRateLimit } from '../../lib/apiComposition'
import { userSchema } from '../../lib/schemas'
async function handler(req, res) {
// Your main logic here
res.status(200).json({ success: true })
}
export default compose(
withRateLimit(100), // 100 requests per hour
withAuth,
withValidation(userSchema)
)(handler)
Best Practices
1. Error Handling
// ✅ Good: Comprehensive error handling
export default async function handler(req, res) {
try {
// Your logic here
const result = await processRequest(req)
res.status(200).json(result)
} catch (error) {
console.error('API Error:', error)
if (error.name === 'ValidationError') {
return res.status(400).json({ error: error.message })
}
if (error.name === 'UnauthorizedError') {
return res.status(401).json({ error: 'Unauthorized' })
}
res.status(500).json({ error: 'Internal server error' })
}
}
2. Input Validation
// ✅ Good: Validate all inputs
import Joi from 'joi'
const userSchema = Joi.object({
name: Joi.string().min(2).max(50).required(),
email: Joi.string().email().required(),
age: Joi.number().min(18).max(120).optional(),
})
export default async function handler(req, res) {
const { error, value } = userSchema.validate(req.body)
if (error) {
return res.status(400).json({
error: 'Validation failed',
details: error.details
})
}
// Use validated data
const { name, email, age } = value
}
3. Rate Limiting
// lib/rateLimit.js
const rateLimitMap = new Map()
export function withRateLimit(limit = 100, windowMs = 60 * 60 * 1000) {
return (handler) => {
return async (req, res) => {
const ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress
const now = Date.now()
const windowStart = now - windowMs
if (!rateLimitMap.has(ip)) {
rateLimitMap.set(ip, [])
}
const requests = rateLimitMap.get(ip)
const validRequests = requests.filter(time => time > windowStart)
if (validRequests.length >= limit) {
return res.status(429).json({ error: 'Too many requests' })
}
validRequests.push(now)
rateLimitMap.set(ip, validRequests)
return handler(req, res)
}
}
}