Beside giving an opinionated approach to React and allow various techniques of server side render, Next.js also offer some server specific features with the most notable being the option to create REST API endpoint within the same codebase.
To explore this concept we will build a simple REST route that:
- expose some activities as JSON
- allow the basic http methods to CRUD (Create Read Update Delete) on activities
To enable persistance but keeping things simple we will use LowDB with the memory connector
to store the data in memory on the server, obviously in the future you may want to switch to a proper database solution connector (MySQL, MongoDB, etc...) especially if we want to build more complex applications than POCs, however the flow of our code would remain more or less the same.
Note: by using memory storage, the data is persistant until the server session is active, so re-deploying the code to Vercel or restarting the server will cause to wipe all the data stored so far.
- create a new
/api
folder within the/pages
folder. Every subfolder created in here will be transformed in a REST route, so to create our endpoint to provide access to excursions activities we will create the following structure:
/pages/api/excursions/[[id]].ts
--> localhost:3000/api/excursions
the [[id]]
is a "catch-all" route to intercept every method and every request to our endpoint, with and without parameters.
- Within the file let's start with the basic structure for a "hello world" endpoint:
import type { NextApiRequest, NextApiResponse } from "next";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
res.status(200).json({ message: "hello world!" });
}
if we now run npm run dev
from terminal ad get the localhost:3000/api/excursions
endpoint with Postman or a regular browser, we can see the JSON response!
The first thing to notice is that the API is "just" a regular function that we are exporting, it is receiving 2 parameters:
req
-> the request object, containing header, URL parameters and all of the data sent to our endpointres
-> the response object, an helper utility to create the data that we want to send back to the API invoker
functions rules the world today...
- Now we can setup our data persistance with LowDB and update the code to return all of the items available
// check the source file for line-by-line comments and proper imports statements
const adapter = new Memory<Excursion[]>();
const db = new Low<Excursion[]>(adapter);
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<Excursion | Excursion[] | Message>
) {
await db.read();
db.data ||= [
{
uuid: uuidv4(),
name: "Mount Nowhere",
height: 2000,
photo: "https://picsum.photos/id/15/1024/768.webp",
timing: 180,
notes:
"First, and succesful attempt! But I definetely need to buy better gear...",
},
];
if (req.method === "GET") {
res.status(200).json(db.data);
}
}
as you can see I've also updated the typing for the response to match the new output.
We are creating are LowDB instance with the memory adapter, creating a new DB and adding the first, hard-coded, item. Then we make sure that only when the request is a GET
we will respond with all of the data in our local DB.
- But we know that a user may want to
GET
only a single entity through a request like/api/excursions/123
, so we can update the body of the main function like this:
const uuid: string =
req.query.id && req.query.id.length > 0 ? req.query?.id[0] : "";
if (req.method === "GET") {
res.status(200).json(db.data);
}
if (req.method === "GET" && uuid.length > 0) {
const getOne: Excursion[] = db.data.filter(
(item: Excursion): boolean => item.uuid === uuid
);
const notFound: Message = {
message: "The provided UUID doesn't match any activity",
};
res.status(200).json(getOne.length > 0 ? getOne[0] : notFound);
}
the req
parameters is very hand to catch eventual data appended to the url thanks to the .query
object.
So we can add a condition to filter out a single entity from the DB or return a "not found" message if the ID isn't found in our collection. It may be a good idea to also respond with a different status
code in that case...
- now let's handle a
POST
request, we don't need the ID here:
if (req.method === "POST") {
const entry: Excursion = req.body;
entry.uuid = uuidv4();
db.data.push(entry);
db.write();
res.status(200).json({ message: "new entry succesfully created" });
}
req.body
is very nice and quick to get the data sent from a client, we are using the uuid
library to create a unique ID to the received data then we can add it and save it to the DB, and return a response to close the request.
Now a client is able to add data to the application.
- the
DELETE
method is easier to handle since we just need to find an item in our DB (which is an array), remove it and update the data and notify the client
if (req.method === "DELETE") {
const remove: Excursion[] = db.data.filter(
(e: Excursion): boolean => e.uuid !== uuid
);
db.data = remove;
db.write();
res
.status(200)
.json({ message: `The item with id ${uuid} has been deleted` });
}
- updating an existing item with the
PUT
method is not particularly difficult, but it mixes most of what we have done so far
if (req.method === "PUT") {
const index: number = db.data.findIndex(
(item: Excursion): boolean => item.uuid === uuid
);
const update: Excursion[] = [...db.data];
update[index] = req.body;
db.data = update;
db.write();
res
.status(200)
.json({ message: `The item with id ${uuid} has been updated` });
}
now our rest API is complete! We can test it locally and do some fine tunings before deploying it on some server.
- but as a very last step before publishing, we need to enable the CORS in our service so that third party domain can access and interact with it
import Cors from "cors";
const cors = Cors({
methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
});
// ...the existing code...
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<Excursion | Excursion[] | Message>
) {
await cors(req, res, () => {});
// ...the existing code...
}
the cors
package makes it quite trivial. We just need to specify which method we want to allow, for safety we have also added the OPTIONS
method since its automatically sent by the browsers at each fetch
request.