Next.js API Routes provide a powerful way to build backend functionality directly within your Next.js application. This means you can create server-side logic for tasks like data fetching and data mutations without needing to set up a separate backend server. This chapter explores how to leverage API Routes for these common backend operations.
API Routes live in the pages/api directory. Each file in this directory becomes an API endpoint. For example, pages/api/users.js will be accessible at /api/users.
export default function handler(req, res) {
res.status(200).json({ name: 'John Doe' });
}The handler function in an API Route receives two arguments: req (the incoming request object) and res (the server response object). You use res to send data back to the client.
One common use case is to expose data from an external API or a database through your API Routes. This allows your frontend to interact with a unified API layer, abstracting away the complexities of where the data actually resides.
Let's imagine we have a pages/api/posts.js file that fetches data from a hypothetical external API. We'll handle GET requests to retrieve a list of posts.
import fetch from 'node-fetch';
export default async function handler(req, res) {
if (req.method === 'GET') {
try {
const response = await fetch('https://jsonplaceholder.typicode.com/posts');
const posts = await response.json();
res.status(200).json(posts);
} catch (error) {
res.status(500).json({ message: 'Error fetching posts', error: error.message });
}
} else {
res.setHeader('Allow', ['GET']);
res.status(405).end(`Method ${req.method} Not Allowed`);
}
}In this example:
- We use
node-fetch(you might need to install this:npm install node-fetchoryarn add node-fetch) to make HTTP requests. - We check
req.methodto ensure we only handleGETrequests. - We fetch data from
jsonplaceholder.typicode.com. - We send the fetched
postsback to the client with a200status code. - Error handling is included to catch potential network issues.
From your Next.js frontend, you can then fetch this data using standard browser fetch API or libraries like swr or react-query.
async function fetchPosts() {
const response = await fetch('/api/posts');
const data = await response.json();
return data;
}
// In your React component:
// const { data: posts, error } = useSWR('/api/posts', fetchPosts);API Routes are equally adept at handling data mutations – operations that change data on the server, such as creating, updating, or deleting records. This typically involves POST, PUT, PATCH, or DELETE HTTP methods.
Let's create an API Route for managing user data, allowing us to create a new user via a POST request. Assume our data is stored in a simple in-memory array for demonstration.
// pages/api/users.js
let users = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
];
export default function handler(req, res) {
if (req.method === 'POST') {
const { name } = req.body;
if (!name) {
return res.status(400).json({ message: 'Name is required' });
}
const newUser = {
id: users.length + 1,
name,
};
users.push(newUser);
res.status(201).json(newUser);
} else if (req.method === 'GET') {
res.status(200).json(users);
} else {
res.setHeader('Allow', ['GET', 'POST']);
res.status(405).end(`Method ${req.method} Not Allowed`);
}
}In this mutation example:
- We define an in-memory
usersarray. - We specifically handle
POSTrequests. - We access the request body using
req.body(Next.js automatically parses JSON bodies). - We validate that a
nameis provided. - A new user object is created with a unique ID.
- The new user is added to our
usersarray. - We send back the
newUserobject with a201(Created) status code.
To trigger this mutation from your frontend, you would again use fetch or a data fetching library, making a POST request with the user data in the request body.
async function createUser(name) {
const response = await fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ name }),
});
const data = await response.json();
return data;
}
// In your React component:
// createUser('Charlie').then(newUser => console.log('User created:', newUser));API Routes can also be dynamic, allowing you to create endpoints that accept parameters. This is useful for fetching or mutating a specific resource, like a single post or user. You create dynamic API routes by using bracket notation in the file name, e.g., pages/api/posts/[id].js.
// pages/api/posts/[id].js
export default function handler(req, res) {
const { id } = req.query; // 'id' will be the value from the URL
if (req.method === 'GET') {
// Fetch a single post by id
// For demo, we'll just return it
res.status(200).json({ id: id, title: `Post ${id}`, content: 'This is a dynamic post.' });
} else {
res.setHeader('Allow', ['GET']);
res.status(405).end(`Method ${req.method} Not Allowed`);
}
}If a user navigates to /api/posts/123, the id variable in req.query will be '123', allowing you to dynamically fetch or manipulate the corresponding data.
graph TD
A[Client Request]
B(API Route: /api/posts/[id])
C{Extract ID from req.query}
D[Fetch/Mutate data based on ID]
E[Send Response]
A --> B
B --> C
C --> D
D --> E
By combining these patterns, you can build robust backend logic for data fetching and mutations directly within your Next.js application, simplifying your development workflow and reducing the need for external API services for many common tasks.