Skip to main content

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.

  1. Create a file named hello.js inside the pages/api directory:
javascript
// 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:

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

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 parameters
  • req.cookies: The cookies sent with the request

The res object provides methods to send back HTTP responses:

  • res.status(code): Sets the HTTP status code
  • res.json(data): Sends a JSON response
  • res.send(data): Sends an HTTP response
  • res.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:

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

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

json
{
"id": "123",
"name": "Product 123"
}

Dynamic API Routes

Similar to dynamic pages, you can create dynamic API routes by using brackets in the filename:

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

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

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

javascript
// 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`);
}
}
javascript
// 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

  1. Keep Security in Mind: Remember that API routes run on the server. Be careful about exposing sensitive information.

  2. Handle Errors Properly: Always include proper error handling in your API routes.

  3. Validate Input Data: Always validate user inputs to prevent security issues.

  4. Use HTTP Methods Correctly: Follow REST conventions (GET for retrieving, POST for creating, PUT for updating, DELETE for deleting).

  5. Set Proper Status Codes: Use appropriate HTTP status codes in your responses.

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

  1. They Only Run on the Server: API routes are only executed on the server, never on the client.

  2. They Don't Participate in Client-Side Bundle: API routes are not included in the JavaScript bundle sent to the browser.

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

  1. Create an API route that returns the current time and date.
  2. Build a weather API route that takes a city name as a query parameter and returns mock weather data.
  3. Implement a full CRUD API for a "blog posts" resource, storing the data in memory.
  4. Create a protected API route that requires an API key passed in the request headers.
  5. Build an API route that uploads and stores a file on the server (you'll need to use a package like multer or formidable).


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)