Next.js API Routes
Introduction
Next.js is not just a framework for building frontend applications; it also allows you to create your own API endpoints right within your Next.js project. This feature, called API Routes, eliminates the need for a separate backend server in many cases, enabling you to build full-stack applications with just Next.js.
API Routes let you create REST API endpoints as Node.js serverless functions that run on the server side. This means you can securely access databases, integrate with external services, process form submissions, and handle authentication—all within your Next.js application.
In this tutorial, we'll explore how to create and use API Routes in Next.js, from basic concepts to more advanced patterns and best practices.
Basic API Route
Creating Your First API Route
API Routes live in the pages/api
directory of your Next.js project. Each file inside this directory is treated as an API endpoint that maps to a route based on its filename.
Let's create a simple API that returns a greeting message:
- Create a file named
hello.js
in thepages/api
directory:
// pages/api/hello.js
export default function handler(req, res) {
res.status(200).json({ message: 'Hello, Next.js!' })
}
- Now you can access this API endpoint at
/api/hello
in your browser or via API calls.
When you make a request to http://localhost:3000/api/hello
, you'll receive the following JSON response:
{
"message": "Hello, Next.js!"
}
Understanding the Handler Function
The API Route handler function takes two parameters:
req
: An instance of http.IncomingMessage, plus some pre-built middlewaresres
: An instance of http.ServerResponse, plus some helper functions
The function signature looks like this:
export default function handler(req, res) {
// Your API logic here
}
Working with HTTP Methods
One of the key features of API Routes is handling different HTTP methods like GET, POST, PUT, DELETE, etc. You can check the method using req.method
and respond accordingly.
Let's create an API endpoint that handles different HTTP methods:
// pages/api/users.js
export default function handler(req, res) {
switch (req.method) {
case 'GET':
// Handle GET request
res.status(200).json({ users: ['John', 'Jane', 'Bob'] })
break
case 'POST':
// Handle POST request
const newUser = req.body.name
// In a real application, you would save to a database here
res.status(201).json({ message: `User ${newUser} created` })
break
default:
// Handle other methods
res.setHeader('Allow', ['GET', 'POST'])
res.status(405).end(`Method ${req.method} Not Allowed`)
}
}
Accessing Request Data
Query Parameters
You can access query parameters from the URL using req.query
:
// pages/api/search.js
export default function handler(req, res) {
const { q } = req.query // If the URL is /api/search?q=nextjs
if (!q) {
return res.status(400).json({ error: 'Query parameter q is required' })
}
// In a real application, you might search a database here
res.status(200).json({ results: `Search results for: ${q}` })
}
Request Body
For POST, PUT, and other methods that include a request body, you can access the data using req.body
:
// pages/api/submit-form.js
export default function handler(req, res) {
if (req.method !== 'POST') {
return res.status(405).json({ message: 'Method not allowed' })
}
const { name, email, message } = req.body
// Validate the data
if (!name || !email || !message) {
return res.status(400).json({ error: 'Missing required fields' })
}
// Process the data (e.g., save to database, send email)
console.log('Form data:', { name, email, message })
// Send response
res.status(200).json({ success: true, message: 'Form submitted successfully' })
}
Dynamic API Routes
Just like Next.js pages, API Routes can be dynamic. This is useful when you want to handle requests for specific resources, like a user profile or a product details.
Create a file named [id].js
in the pages/api/products
directory:
// pages/api/products/[id].js
export default function handler(req, res) {
const { id } = req.query
// In a real application, you would fetch from a database
const productData = {
1: { id: 1, name: 'Product 1', price: 19.99 },
2: { id: 2, name: 'Product 2', price: 29.99 },
3: { id: 3, name: 'Product 3', price: 39.99 }
}
if (!productData[id]) {
return res.status(404).json({ error: 'Product not found' })
}
res.status(200).json(productData[id])
}
Now, you can access product information by making requests to paths like:
/api/products/1
/api/products/2
/api/products/3
Catch-All API Routes
For even more flexibility, you can use catch-all routes with [...param]
syntax:
// pages/api/posts/[...slug].js
export default function handler(req, res) {
const { slug } = req.query
// slug will be an array of path segments
console.log(slug) // e.g., ['2022', '01', 'hello-world']
res.status(200).json({ slug })
}
This will match routes like:
/api/posts/2022/01/hello-world
/api/posts/category/technology
API Middleware
You can create custom middleware for your API routes to handle tasks like authentication, logging, or error handling.
Here's an example of a simple authentication middleware:
// middleware/withAuth.js
export function withAuth(handler) {
return async (req, res) => {
// Check for auth token
const authToken = req.headers.authorization
if (!authToken || !authToken.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Unauthorized' })
}
// In a real app, you would verify the token
// For example: const user = await verifyToken(authToken.split(' ')[1])
// If valid, call the original handler
return handler(req, res)
}
}
Then use it in your API route:
// pages/api/protected-data.js
import { withAuth } from '../../middleware/withAuth'
function handler(req, res) {
// This code only runs if authentication passes
res.status(200).json({ data: 'This is protected data' })
}
export default withAuth(handler)
Real-World Example: Contact Form API
Let's build a practical example: a contact form submission API that validates input, sends an email notification, and stores the submission in a database.
// pages/api/contact.js
import { sendEmail } from '../../lib/email'
import { saveToDatabase } from '../../lib/db'
export default async function handler(req, res) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' })
}
try {
const { name, email, subject, message } = req.body
// Validate input
if (!name || !email || !message) {
return res.status(400).json({
error: 'Missing required fields',
validationErrors: {
name: !name ? 'Name is required' : null,
email: !email ? 'Email is required' : null,
message: !message ? 'Message is required' : null
}
})
}
if (!/^\S+@\S+\.\S+$/.test(email)) {
return res.status(400).json({
error: 'Invalid email address'
})
}
// Save to database
const submissionId = await saveToDatabase({
name,
email,
subject: subject || 'No subject',
message,
date: new Date()
})
// Send notification email
await sendEmail({
to: '[email protected]',
subject: `New contact form submission: ${subject || 'No subject'}`,
text: `
New contact form submission:
Name: ${name}
Email: ${email}
Subject: ${subject || 'No subject'}
Message: ${message}
`
})
// Return success response
res.status(200).json({
success: true,
message: 'Form submitted successfully',
submissionId
})
} catch (error) {
console.error('Contact form error:', error)
res.status(500).json({
error: 'An error occurred while processing your request'
})
}
}
Here's how you might use this API from a React component:
// components/ContactForm.jsx
import { useState } from 'react'
export default function ContactForm() {
const [formData, setFormData] = useState({
name: '',
email: '',
subject: '',
message: ''
})
const [status, setStatus] = useState({
submitting: false,
submitted: false,
error: null
})
const handleChange = (e) => {
setFormData({
...formData,
[e.target.name]: e.target.value
})
}
const handleSubmit = async (e) => {
e.preventDefault()
setStatus({ submitting: true, submitted: false, error: null })
try {
const response = await fetch('/api/contact', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
})
const data = await response.json()
if (!response.ok) {
throw new Error(data.error || 'Something went wrong')
}
// Form submitted successfully
setStatus({
submitting: false,
submitted: true,
error: null
})
// Reset form
setFormData({
name: '',
email: '',
subject: '',
message: ''
})
} catch (error) {
setStatus({
submitting: false,
submitted: false,
error: error.message
})
}
}
return (
<div className="contact-form">
<h2>Contact Us</h2>
{status.submitted && (
<div className="success-message">
Thank you for your message! We'll get back to you soon.
</div>
)}
{status.error && (
<div className="error-message">
Error: {status.error}
</div>
)}
<form onSubmit={handleSubmit}>
<div className="form-group">
<label htmlFor="name">Name*</label>
<input
type="text"
id="name"
name="name"
value={formData.name}
onChange={handleChange}
required
/>
</div>
<div className="form-group">
<label htmlFor="email">Email*</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleChange}
required
/>
</div>
<div className="form-group">
<label htmlFor="subject">Subject</label>
<input
type="text"
id="subject"
name="subject"
value={formData.subject}
onChange={handleChange}
/>
</div>
<div className="form-group">
<label htmlFor="message">Message*</label>
<textarea
id="message"
name="message"
value={formData.message}
onChange={handleChange}
rows="5"
required
/>
</div>
<button
type="submit"
disabled={status.submitting}
>
{status.submitting ? 'Sending...' : 'Send Message'}
</button>
</form>
</div>
)
}
API Routes vs. Server Components
In Next.js 13 and above, Server Components have become a powerful feature for server-side rendering. It's important to understand when to use API Routes versus Server Components:
-
API Routes: Use when you need to create RESTful endpoints that other clients (mobile apps, other websites) can access, or when you need to handle form submissions and AJAX requests.
-
Server Components: Use when you're fetching data primarily for your own Next.js application's UI rendering. Server Components can directly access databases and external services without exposing an API.
Best Practices
-
Keep API Logic Separate: Store complex logic in separate files outside the API route handler for better organization and testability.
-
Handle Errors Properly: Always use try-catch blocks and return appropriate status codes.
-
Validate Input Data: Always validate and sanitize incoming data to prevent security issues.
-
Set Proper Headers: Use appropriate status codes and CORS headers when needed.
-
Optimize Performance: Use caching strategies for expensive operations.
-
Security First: Never trust client inputs and protect sensitive endpoints with authentication.
-
Rate Limiting: Implement rate limiting for public APIs to prevent abuse.
Limitations
API Routes have a few limitations to be aware of:
- They only run on the server side, not during the static site generation process
- They're deployed as serverless functions, which have cold start times
- They have timeout limits (typically 10 seconds on Vercel)
- They don't support streaming responses
Summary
Next.js API Routes provide a powerful way to build backend functionality right within your Next.js application. They allow you to create RESTful endpoints, handle form submissions, interact with databases, and more—all without setting up a separate backend server.
In this guide, we've covered:
- Creating basic API routes
- Handling different HTTP methods
- Working with request data (query parameters and body)
- Creating dynamic and catch-all routes
- Using middleware for authentication
- Building a practical contact form API example
- Best practices and limitations
With API Routes, Next.js becomes a true full-stack framework, enabling developers to build complete web applications with a unified codebase.
Additional Resources
- Official Next.js API Routes Documentation
- Next.js API Middleware Documentation
- Next.js API Response Helpers
- Using MongoDB with Next.js API Routes
- Authentication in Next.js API Routes
Exercises
- Create an API route that returns the current time and date.
- Build a basic CRUD API for a todo list application using API routes and an in-memory array.
- Create a dynamic API route that accepts an ID parameter and returns details about a specific item.
- Implement authentication middleware to protect certain API routes.
- Build a file upload API endpoint that saves uploaded files to the server.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)