Cody Jarrett
Posted on September 20, 2021
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
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,
},
];
Your pages
directory structure should now look like the following:
- pages
- /api
- /person
- [id].js
- index.js
- /person
- [id].js
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);
}
}
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>
);
}
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,
},
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);
}
}
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;
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>
))}
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.
Posted on September 20, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.