DEV Community

Cover image for Understanding how API routes work in Next.js
Cody Jarrett
Cody Jarrett

Posted on

Understanding how API routes work in Next.js

Next.js makes it really simple for developers at any skill level to build API's whether with REST or GraphQL. I think GraphQL is really cool but for the purposes of simplicity I will focus on building API routes in REST. If you aren't already familiar with REST, REST stands for REpresentational State Transfer. In short, REST is a type of API that conforms to the design principles of the representational state transfer architectural style. And an API built correctly in REST is considered what's called Restful. Check out more readings on REST here.

At a high level, normally, when building a full stack application, let's say a MERN (MongoDB, Express, React and Node) application you'll probably create some separation between both your client and your server code. You'll probably create some server directory and in that directory you'll create a standalone express server that then has a bunch of routes that will perform all of your various CRUD (Create, Read, Update and Delete) operations on your database. Then in your client code you'll make GET/POSTS/PUT/DELETE requests to those various routes you've created server-side. Sometimes trying to follow how both the client and server code talk to each other can be really confusing.

Luckily, Next.js to the rescue 🙌. Next.js reduces this confusion and makes it pretty simple to create API routes that map to a particular file created in the pages/api directory structure. Let's walk through it.

Quick note: We won't focus on actually hitting a live database in this article. The main point I want to get across is how simple API's can be built in Next.js. Hopefully with this simple mental model any developer should be able to expand on this information and create more complex applications.

The code for this article can also be found in this sandbox

Let's start by creating a new Next.js application using the following command in your terminal.

npx create-next-app
#or
yarn create next-app
Enter fullscreen mode Exit fullscreen mode

You'll be asked to create a name for the project - just pick something 😎. After all the installation is complete, start the development server by running npm run dev or yarn dev in your terminal.

At this point, you should be able to visit http://localhost:3000 to view your application.

Now that everything is running, let's head over to the pages/api directory. Inside of this directory create a new person directory. And inside of the person directory create two files index.js and [id].js (we'll touch on this bracket syntax soon). Inside of the pages root directory, create another person directory with one file named [id].js in it. Lastly, in the root of your entire application, create a data.js file with the following code:

export const data = [
  {
    id: 1,
    firstName: "LeBron",
    middleName: "Raymone",
    lastName: "James",
    age: 36,
  },
  {
    id: 2,
    firstName: "Lil",
    middleName: "Nas",
    lastName: "X",
    age: 22,
  },
  {
    id: 3,
    firstName: "Beyoncé",
    middleName: "Giselle",
    lastName: "Knowles-Carter",
    age: 40,
  },
];
Enter fullscreen mode Exit fullscreen mode

Your pages directory structure should now look like the following:

- pages
  - /api
    - /person
      - [id].js
      - index.js  
  - /person
    - [id].js  
Enter fullscreen mode Exit fullscreen mode

Any file inside the folder pages/api is automatically mapped to /api/* and will be treated as an API endpoint instead of a client-side page. Also, no need to worry about your client-side bundle size, these files are server-side bundled and won't increase the code size going to the browser.

In the index.js file you just created in the person directory, paste the following snippet into your editor:

import { data } from "../../../data";

export default function handler(request, response) {
  const { method } = request;

  if (method === "GET") {
    return response.status(200).json(data);
  }

  if (method === "POST") {
    const { body } = request;
    data.push({ ...body, id: data.length + 1 });
    return response.status(200).json(data);
  }
}
Enter fullscreen mode Exit fullscreen mode

Let's break this code down - for an API route to work you need to export a function, that receives two parameters: request: an instance of http.IncomingMessage and response: an instance of http.ServerResponse. Inside of this request handler you can handle different HTTP methods in an API route by using request.method which determines what HTTP method is being used by the request. In this code snippet, we are expecting either a GET or POST request. If we receive a GET request we will simply send a status of 200 and return the data in json form. If a POST request is received we will add what ever is sent over from the client via the body on the request to our array of data. You can think of this as if you were to perform a create operation on your database. Once we've completed this operation we will then also return a status of 200 and the current state of the data in json form.

Now let's head over to pages/index.js, you should find a bunch of jsx that's been provided by Next to render their custom home page. ERASE ALL OF IT 😈. And replace with the following code snippet:

import Link from "next/link";
import { useReducer, useState } from "react";

function reducer(state, action) {
  switch (action.type) {
    case "UPDATE_FIRST_NAME":
      return {
        ...state,
        firstName: action.payload.firstName
      };
    case "UPDATE_MIDDLE_NAME":
      return {
        ...state,
        middleName: action.payload.middleName
      };
    case "UPDATE_LAST_NAME":
      return {
        ...state,
        lastName: action.payload.lastName
      };
    case "UPDATE_AGE":
      return {
        ...state,
        age: action.payload.age
      };
    case "CLEAR":
      return initialState;
    default:
      return state;
  }
}

const initialState = {
  firstName: "",
  middleName: "",
  lastName: "",
  age: ""
};

export default function Home() {
  const [state, dispatch] = useReducer(reducer, initialState);
  const [data, setData] = useState([]);

  const fetchData = async () => {
    const response = await fetch("/api/person");

    if (!response.ok) {
      throw new Error(`Error: ${response.status}`);
    }
    const people = await response.json();
    return setData(people);
  };

  const postData = async () => {
    const response = await fetch("/api/person", {
      method: "POST",
      headers: {
        "Content-Type": "application/json"
      },
      body: JSON.stringify(state)
    });

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

    dispatch({ type: "CLEAR" });
    const people = await response.json();
    return setData(people);
  };
  return (
    <div style={{ margin: "0 auto", maxWidth: "400px" }}>
      <div style={{ display: "flex", flexDirection: "column" }}>
        <label htmlFor="firstName">First Name</label>
        <input
          type="text"
          id="firstName"
          value={state.firstName}
          onChange={(e) =>
            dispatch({
              type: "UPDATE_FIRST_NAME",
              payload: { firstName: e.target.value }
            })
          }
        />
        <label htmlFor="middleName">Middle Name</label>
        <input
          type="text"
          id="middleName"
          value={state.middleName}
          onChange={(e) =>
            dispatch({
              type: "UPDATE_MIDDLE_NAME",
              payload: { middleName: e.target.value }
            })
          }
        />
        <label htmlFor="lastName">Last Name</label>
        <input
          type="text"
          id="lastName"
          value={state.lastName}
          onChange={(e) =>
            dispatch({
              type: "UPDATE_LAST_NAME",
              payload: { lastName: e.target.value }
            })
          }
        />
        <label htmlFor="age">Age</label>
        <input
          type="text"
          id="age"
          value={state.age}
          onChange={(e) =>
            dispatch({
              type: "UPDATE_AGE",
              payload: { age: e.target.value }
            })
          }
        />
      </div>
      <div
        style={{ marginTop: "1rem", display: "flex", justifyContent: "center" }}
      >
        <button onClick={fetchData}>FETCH</button>
        <button onClick={postData}>CREATE</button>
      </div>
      <div>Data:</div>
      {data ? <pre>{JSON.stringify(data, null, 4)}</pre> : null}
      {data.length > 0 ? (
        <div style={{ textAlign: "center" }}>
          Click a button to go to individual page
          <div
            style={{
              marginTop: "1rem",
              display: "flex",
              justifyContent: "center"
            }}
          >
            {data.map((person, index) => (
              <Link
                key={index}
                href="/person/[id]"
                as={`/person/${person.id}`}
                passHref
              >
                <span
                  style={{
                    padding: "5px 10px",
                    border: "1px solid black"
                  }}
                >{`${person.firstName} ${person.lastName}`}</span>
              </Link>
            ))}
          </div>
        </div>
      ) : null}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Hopefully at this point you are pretty familiar with what's going on here. It's pretty basic React code. If you need to brush up on your React, head over to the documentation. The main things I want to point out here are the fetchData and postData handlers. You'll notice that they are both performing fetch requests on the api/person endpoint that we created previously. As a reminder this is client-side code here so we can fetch just using the absolute path of api/person. The same doesn't apply for server-side rendering requests and we will touch on that soon.

Voilà 👌 - this is the bread and butter of API routes in Next.js.

Open up your network tab in the devtools of your browser.

When you click the FETCH button in the UI, you'll notice a GET request is made to api/person and the response is the data that we hard-coded.

{
      id: 1,
      firstName: "LeBron",
      middleName: "Raymone",
      lastName: "James",
      age: 36,
    },
    { 
      id: 2, 
      firstName: "Lil", 
      middleName: "Nas", 
      lastName: "X", 
      age: 22 
    },
    {
      id: 3,
      firstName: "Beyoncé",
      middleName: "Giselle",
      lastName: "Knowles-Carter",
      age: 40,
},
Enter fullscreen mode Exit fullscreen mode

You'll also notice that a POST request is sent if you fill out the form inputs and click the CREATE button.

Again, you can imagine that in your API code you're performing some read and create operations on your database and returning the expected data. For this example I wanted to keep it simple.

Let's head over to the pages/person/[id].js file and paste this snippet into the editor:

import { data } from "../../../data";

export default function handler(request, response) {
  const { method } = request;

  if (method === "GET") {
    const { id } = request.query;

    const person = data.find((person) => person.id.toString() === id);

    if (!person) {
      return response.status(400).json("User not found");
    }

    return response.status(200).json(person);
  }
}
Enter fullscreen mode Exit fullscreen mode

You might be wondering, what's up with the bracket syntax? Well, the short of it is Next.js provides a way for developers to create dynamic routing. The text that you put in between the brackets work as a query parameter that you have access to from the browser url. More info on dynamic routes can be found in the docs. Breaking down this snippet above we are expecting a GET request that will carry an id on the request.query object. Once we have access to this id we can then search our "database" for a person whose id matches the id provided by the request. If we find a person then we return it in json format with a status of 200. If not, we return an error of 400 with a message User not found. However, there's still one more step. Remember this is just the api step, we still need to render a page for our individual person.

Let's hop over to person/[id].js and paste the following code snippet:

import { useRouter } from "next/router";

const Person = ({ user }) => {
  const router = useRouter();

  return (
    <div>
      <button onClick={() => router.back()}>Back</button>
      <pre>{JSON.stringify(user, null, 4)}</pre>
    </div>
  );
};

export async function getServerSideProps(context) {
  const { id } = context.params;
  const user = await fetch(`http://localhost:3000/api/person/${id}`);
  const data = await user.json();

  if (!data) {
    return {
      notFound: true
    };
  }

  return {
    props: { user: data }
  };
}

export default Person;
Enter fullscreen mode Exit fullscreen mode

Let's break this down - if we look back at pages/index.js you'll find the following snippet:

{data.map((person, index) => (
               <Link
                key={index}
                href="/person/[id]"
                as={`/person/${person.id}`}
                passHref
              >
                <span
                  style={{
                    padding: "5px 10px",
                    border: "1px solid black"
                  }}
                >{`${person.firstName} ${person.lastName}`}</span>
              </Link>
))}
Enter fullscreen mode Exit fullscreen mode

You'll notice that we are mapping over each person in our data and rendering Link tags for each of them. Next.js provides Link tags that can be used for client-side transitions between routes. In our case we are expecting each Link to transition to person/[id] page, the id being the one provided on each person object. So when the user clicks one of these links, Next.js will transition to the appropriate page, for example person/2.

By default, Next.js pre-renders every page. This means that Next.js will create HTML for each page in advance, instead of having it all done via client-side Javascript. You can pre-render either by Static Generation or Server-side rendering. Since our app relies on "frequently updated data fetched from an external API" we will go the server-side rendering route.

This leads us back to the person/[id].js file. You'll notice that we are exporting an async function called getServerSideProps. This is one of helper functions that Next.js provides us for pre-rendering a server-side page. Each request will pre-render a page on each request using the data return back from this function. The context parameter is an object that contains useful information that can be used to in this function. In our case we want to get access to the id that's been passed in the request using the context.params object. More information on the context parameter here.

Once we have access to the id we make a fetch request to http://localhost:3000/api/person${id}. Notice we have to provide the full absolute url including the scheme (http://), host (localhost:) and port (3000). That's because this request is happening on the server not the client. You have to use an absolute URL in the server environment NOT relative. Once the request is successful, we format the data to json and check whether we have data or not. If not, we return an object with notFound: true. This is some Next.js magic that tells the component to render a 404 status page. Otherwise, if we have data we will return a prop object which will be passed to the page components as props. In our case we will pass along the data we've received from the request to the Person component. In our Person component, we are destructioning user off of the props and using it to display.

And that's pretty much it. There's a ton more detail I could've delved into but hopefully on a high level you now have a better understanding of how Next.js API routes work.

Connect with me on Twitter and on LinkedIn

Discussion (0)