Skip to content

Building full-stack React apps with Next.js API routes

Besides Next.js being a great front-end framework for React, it also provides a simple way to build an API for your front-end - all in the same web app!

As of version 9, Next.js provides API routes that allows developers to create API endpoints using the Next.js folder structure. We can use API endpoints to build either a RESTful API, or a GraphQL API. This article will focus on a RESTful API for the sake of simplicity.

With Next.js API routes, there's no longer a need to set up a back-end Node.js server to build an API. Building out two separate projects for the front-end and back-end of an application introduces its own set of challenges. Next.js simplifies this process by becoming a full-stack web framework with the addition of API routes. Building and deploying a full-stack web app has never been faster and easier!

Setting up API routes

API routes go in the pages/api directory of a Next.js project.

When a file or folder is added to this pages/api folder, Next.js will create an API endpoint URL for it. Creating a pages/api/users.ts file will create an /api/users API endpoint. We can also create an /api/users API endpoint by creating a pages/api/users/index.ts file.

To create a dynamic API route for a specific user, we can create a pages/api/users/[id].ts file. This dynamic route will match requests such as /api/users/1.

Just like the pages folder maps its files and folders to URLs that can be visited in a web browser, the pages/api folder maps its files and folders to API endpoint URLs.

Creating API routes

To begin, let's create a new Next.js project called nextjs-full-stack.

npx create-next-app full-stack-nextjs

cd full-stack-nextjs

If you prefer using TypeScript with Next.js, you can use npx create-next-app --ts instead. For this article, we'll just use JavaScript.

Notice that newly created Next.js project contains a pages/api/hello.js file. An /api/hello API endpoint has already been provided for us. Let's run the project using npm run dev to try this API endpoint.

Visit http://localhost:3000/api/hello in a web browser. You should see the following response.

{ "name": "John Doe" }

Understanding API routes

Let's take a look at the code in the pages/api/hello.js file.

export default function handler(req, res) {
  res.status(200).json({ name: 'John Doe' })
}

The default export in files that are found within the api folder must be a function. Each function is a handler for a particular route. Asynchronous functions are also supported for cases where requests need to be made to external APIs or a database.

When running the Next.js project locally, it creates a Node.js server and passes the request and response objects from the server into the handler function of the requested endpoint.

A dynamic route

Let's create a pages/api/users/[id].js file to create an API route for a specific user. Let's add the following code within this file.

const users = [
	{ id: 1, name: 'John Smith' },
	{ id: 2, name: 'Jane Doe' },
];

export default (req, res) => {
  const { query: { id } } = req;

  res.json({ 
    ...users.find(user => user.id === parseInt(id)),
  });
}

For the purposes of creating an example, we mocked up a list of users within the file. More realistic usage might involve querying a database for the requested user.

The id value is retrieved from the request query parameter, and then used to find the corresponding user object in the list of users. Keep in mind that the id from the request is a string, so we must parse it to an integer in order to perform a user lookup by id on the users list.

Visit http://localhost:3000/api/users/1 in a web browser. You should see the following response.

{ "id": 1, "name": "John Smith" }

Returning an error

We can modify the user endpoint we just created to return an error when the requested user id is not found in a list of users.

const users = [
	{ id: 1, name: 'John Smith' },
	{ id: 2, name: 'Jane Doe' },
];

export default (req, res) => {
  const { query: { id } } = req;

  const user = users.find(user => user.id === parseInt(id));
  if (!user) {
    return res.status(404).json({
      status: 404,
      message: 'Not Found'
    });
  }

  res.json({ ...user });
}

Visit http://localhost:3000/api/users/3 in a web browser. You should see the following response since no user with an id of 3 exists.

{ "status": 404, "message": "Not Found" }

Handling multiple HTTP verbs

Next.js API routes allow us to support multiple HTTP verbs for the same endpoint all in one file. The HTTP verbs that we want to support for a single API endpoint are specified in the request handler function.

Let's create a pages/api/users/index.js file to create an API route for all users. We want this endpoint to support GET requests to get all users and POST requests to create users. Let's add the following code within this file.

export default (req, res) => {
  const { method } = req;

  switch (method) {
    case 'GET':
      res.json({ method: 'GET', endpoint: 'Users' });
      break;
    case 'POST':
      res.json({ method: 'POST', endpoint: 'Users' });
      break;
    default:
      res.setHeader('Allow', ['GET', 'POST']);
      res.status(405).end(`Method ${method} Not Allowed`);
      break;
  }
}

The req.method value will tell us what HTTP verb was used to make the request. We can use a switch statement to support multiple HTTP verbs for the same endpoint. Any HTTP requests that are not GET or POST, requests will enter the default case and return a 405 Method Not Allowed error.

Visit http://localhost:3000/api/users in a web browser. You should see the following response.

{ "method": "GET", "endpoint": "Users" }

We can use an API platform like Postman or Insomnia to test POST, PUT, and DELETE requests to this API endpoint.

Testing the POST request will return the following response.

{
  "method": "POST",
  "endpoint": "Users"
}

Testing PUT and DELETE requests will return Method Not Allowed errors with a 405 Method Not Allowed response code.

Using API routes from the front-end

Let's create a users/[id].js page to retrieve a specific user when the page loads, and then mimic a save profile operation when the form on the page is submitted.

We will use client-side rendering within Next.js for this page. Client-side rendering is when the client makes requests for API data. The approach used in this example can also apply for pages that use server-side Rendering (SSR).

import { useEffect, useState } from 'react';
import { useRouter } from 'next/router';

export default function User() {
  const router = useRouter();
  const { id } = router.query;
  const [name, setName] = useState();

  // GET request to get a user
  useEffect(() => {
    // wait for the useRouter hook to asynchronously get the query id
    if (!id) {
      return;
    }

    const fetchUser = async () => {
      const response = await fetch(`/api/users/${id}`, {
        method: "GET",
        headers: {
          "Content-Type": "application/json"
        },
      });

      if (!response.ok) {
        throw new Error(`Error: ${response.status}`);
      }

      const user = await response.json();
      setName(user?.name);
    }

    fetchUser();
  }, [id]);

  // POST request to mimic the saving of a user
  const onSubmit = async (e) => {
    e.preventDefault();
    const response = await fetch("/api/users", {
      method: "POST",
      headers: {
        "Content-Type": "application/json"
      },
      body: JSON.stringify({}),
    });

    if (!response.ok) {
      throw new Error(`Error: ${response.status}`);
    }

    const data = await response.json();
    console.log('POST: ', data);
  };

  return (
    <div>
      <h1>User Form</h1>
      <form onSubmit={onSubmit}>
        <div>
          <label htmlFor="name">Name</label>
          <input 
            type="text" 
            id="name" 
            name="name" 
            value={name ?? ''} 
            onChange={(e) => setName(e.target.value)}
          />
        </div>
        <button type="submit">Submit</button>
      </form>
    </div>
  );
}

To retrieve a user by id when the page loads, we can use the React useEffect hook with the id as a dependency. The browser-supported Fetch API is used to make a GET request to /api/users/[id]. Async/await is used to wait for this asynchronous request to complete. Once it is completed, the user's name is saved to the component state using setName, and displayed within the input text box.

When the form is submitted, a POST request is made to the /api/users API route that we created earlier. To keep things simple, the response of the request is then logged to the console.

Conlusion

Next.js makes it fast and easy to build an API for your next project. A very good use case to get started with Next.js API routes is for the implementation of CRUD operations to handle form input.

API endpoints are created as Node.js serverless functions. You can build out your entire application's API with Next.js API routes. Go ahead and build something awesome with them!