Next.js API Routes Basics
Introduction
API routes provide a solution to build your API directly inside a Next.js application. Instead of requiring a separate backend server, Next.js allows you to create serverless API endpoints as part of your application. This means you can handle both your frontend and backend in a single project, making development more streamlined and deployment simpler.
In this tutorial, we'll explore the fundamentals of Next.js API routes, learn how to create them, and understand how they can be used in real-world applications.
What are API Routes?
API routes are files placed in the special pages/api
directory (or app/api
in the App Router). Any file inside this directory is treated as an API endpoint rather than a page. They are server-side only bundles and won't increase your client-side bundle size.
These routes follow the same file-based routing mechanism as pages in Next.js:
pages/api/hello.js
→/api/hello
pages/api/users/[id].js
→/api/users/:id
Creating Your First API Route
Let's start by creating a simple API endpoint that returns a JSON response.
- Create a file named
hello.js
inside thepages/api
directory:
// pages/api/hello.js
export default function handler(req, res) {
res.status(200).json({ message: 'Hello from Next.js!' });
}
This creates an API route accessible at /api/hello
that returns a JSON object with a greeting message.
When you visit http://localhost:3000/api/hello
in your browser, you'll see:
{
"message": "Hello from Next.js!"
}
Understanding the API Handler Function
Each API route exports a default function called a handler. This function receives two parameters:
req
: An instance of http.IncomingMessage, plus some pre-built middlewaresres
: An instance of http.ServerResponse, plus some helper functions
The req
object contains information about the HTTP request, such as:
req.method
: The HTTP method used (GET, POST, etc.)req.body
: The request body (if any)req.query
: The URL query parametersreq.cookies
: The cookies sent with the request
The res
object provides methods to send back HTTP responses:
res.status(code)
: Sets the HTTP status coderes.json(data)
: Sends a JSON responseres.send(data)
: Sends an HTTP responseres.redirect(url)
: Redirects to a specified URL
Handling Different HTTP Methods
API routes can handle different HTTP methods (GET, POST, PUT, DELETE, etc.) in a single file. Here's how to create an endpoint that responds differently based on the HTTP method:
// pages/api/users.js
export default function handler(req, res) {
const { method } = req;
switch (method) {
case 'GET':
// Handle GET request
res.status(200).json({ users: ['John Doe', 'Jane Smith'] });
break;
case 'POST':
// Handle POST request
const { name } = req.body;
res.status(201).json({ message: `User ${name} created successfully` });
break;
default:
res.setHeader('Allow', ['GET', 'POST']);
res.status(405).end(`Method ${method} Not Allowed`);
}
}
Accessing Query Parameters
Next.js makes it easy to access query parameters in your API routes:
// pages/api/product.js
export default function handler(req, res) {
const { id } = req.query;
res.status(200).json({ id, name: `Product ${id}` });
}
If you visit /api/product?id=123
, you'll receive:
{
"id": "123",
"name": "Product 123"
}
Dynamic API Routes
Similar to dynamic pages, you can create dynamic API routes by using brackets in the filename:
// pages/api/products/[id].js
export default function handler(req, res) {
const { id } = req.query;
// In a real app, you would fetch data from a database
res.status(200).json({
id,
name: `Product ${id}`,
price: 99.99,
description: 'A fantastic product'
});
}
Now you can access /api/products/123
to get information about product 123.
Handling Form Data
API routes are perfect for handling form submissions. Here's an example of a simple contact form handler:
// pages/api/contact.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({ message: 'Missing required fields' });
}
// In a real application, you would save this to a database
// or send an email
console.log('Contact form submission:', { name, email, message });
// Return success response
res.status(200).json({ message: 'Form submitted successfully' });
}
Real-World Example: A Simple Todo API
Let's build a more complete example with a Todo API that supports creating, reading, updating, and deleting todos (CRUD operations).
First, we'll create a simple in-memory data store (in a real application, you would use a database):
// lib/todos.js
// This is just a simple in-memory store for demonstration
let todos = [
{ id: 1, text: 'Learn Next.js', completed: false },
{ id: 2, text: 'Build an API', completed: false },
{ id: 3, text: 'Deploy app', completed: false },
];
let nextId = 4;
export function getAllTodos() {
return todos;
}
export function getTodoById(id) {
return todos.find(todo => todo.id === parseInt(id));
}
export function createTodo(text) {
const newTodo = { id: nextId++, text, completed: false };
todos.push(newTodo);
return newTodo;
}
export function updateTodo(id, updates) {
const todoIndex = todos.findIndex(todo => todo.id === parseInt(id));
if (todoIndex === -1) return null;
todos[todoIndex] = { ...todos[todoIndex], ...updates };
return todos[todoIndex];
}
export function deleteTodo(id) {
const todoIndex = todos.findIndex(todo => todo.id === parseInt(id));
if (todoIndex === -1) return false;
todos.splice(todoIndex, 1);
return true;
}
Now, let's create our API routes:
// pages/api/todos/index.js
import { getAllTodos, createTodo } from '../../../lib/todos';
export default function handler(req, res) {
switch (req.method) {
case 'GET':
// Get all todos
return res.status(200).json(getAllTodos());
case 'POST':
// Create a new todo
const { text } = req.body;
if (!text) {
return res.status(400).json({ message: 'Text field is required' });
}
const newTodo = createTodo(text);
return res.status(201).json(newTodo);
default:
res.setHeader('Allow', ['GET', 'POST']);
return res.status(405).end(`Method ${req.method} Not Allowed`);
}
}
// pages/api/todos/[id].js
import { getTodoById, updateTodo, deleteTodo } from '../../../lib/todos';
export default function handler(req, res) {
const { id } = req.query;
switch (req.method) {
case 'GET':
// Get a specific todo
const todo = getTodoById(id);
if (!todo) {
return res.status(404).json({ message: 'Todo not found' });
}
return res.status(200).json(todo);
case 'PUT':
// Update a todo
const updatedTodo = updateTodo(id, req.body);
if (!updatedTodo) {
return res.status(404).json({ message: 'Todo not found' });
}
return res.status(200).json(updatedTodo);
case 'DELETE':
// Delete a todo
const deleted = deleteTodo(id);
if (!deleted) {
return res.status(404).json({ message: 'Todo not found' });
}
return res.status(204).end();
default:
res.setHeader('Allow', ['GET', 'PUT', 'DELETE']);
return res.status(405).end(`Method ${req.method} Not Allowed`);
}
}
Best Practices for API Routes
-
Keep Security in Mind: Remember that API routes run on the server. Be careful about exposing sensitive information.
-
Handle Errors Properly: Always include proper error handling in your API routes.
-
Validate Input Data: Always validate user inputs to prevent security issues.
-
Use HTTP Methods Correctly: Follow REST conventions (GET for retrieving, POST for creating, PUT for updating, DELETE for deleting).
-
Set Proper Status Codes: Use appropriate HTTP status codes in your responses.
-
Structure Your API Routes Logically: Organize your API routes in a way that makes sense for your application.
Limitations of API Routes
While API routes are powerful, they do have some limitations:
-
They Only Run on the Server: API routes are only executed on the server, never on the client.
-
They Don't Participate in Client-Side Bundle: API routes are not included in the JavaScript bundle sent to the browser.
-
They are Serverless Functions: In production, API routes are deployed as serverless functions, which may have limitations depending on your hosting provider.
Summary
Next.js API routes provide a powerful way to build your backend directly within your Next.js application. They:
- Allow you to write server-side code without setting up a separate backend
- Follow the same file-based routing system as pages
- Support various HTTP methods
- Can handle dynamic routes and query parameters
- Run only on the server, never on the client
In this tutorial, we've covered the basics of creating and using API routes in Next.js, from simple JSON responses to a complete CRUD API for a todo application.
Additional Resources
Exercises
- Create an API route that returns the current time and date.
- Build a weather API route that takes a city name as a query parameter and returns mock weather data.
- Implement a full CRUD API for a "blog posts" resource, storing the data in memory.
- Create a protected API route that requires an API key passed in the request headers.
- Build an API route that uploads and stores a file on the server (you'll need to use a package like
multer
orformidable
).
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)