Explore Next.js 9 API Routes
I have just found out about the new API Routes feature that is released in Next.js 9. This blog post tells you my first glance of it.
At the moment, I'm going on my vacation at Phú Quốc island. I do not get to stay in a resort quite often, so this experience is quite unforgettable.
Anyway, last night, I opened Reddit and found out that Next.js 9 was released. Even though I am using Next.js for my LuvBit Studio website, I do not quite follow along with the development of the framework. I decided to check out the news.
You can read about it here.
What caught my eye was API Routes.
In this post, I am going to show you some aspects of it, including File-system routing, Middlewares and helpers, and Dynamic routing.
File-system routing
For some background, file-system routing allows me to "serve each file in /pages under a pathname matching the filename." For example, in my project, I have a login.jsx
under /pages. When I visit my website at /login
, the server serves login.jsx
. Better yet, when I need a page at /profile/edit
(Edit Profile Page), I simply make a folder called profile
under /pages
then add edit.jsx
.
For Next.js 9's API routes, I simply put my files under /pages/api/
. If I need a function at /api/accounts/updateprofile
, the file will be at /pages/api/accounts/updateprofile.js
.
Why is Next.js's File-system routing good?
Right now, I use Express.js backend for my APIs. If I need to accomplish the task above, my code will be:
const express = require("express");
const User = require("../../models/userModel");
app.post("/api/accounts/updateprofile", (req, res) => {
const userinfo = req.body;
return User.findById(req.body.userId).then(user =>
user.updateOne({ ...userinfo })
);
});
With other functions, the code will be:
const express = require("express");
const User = require("../../models/userModel");
app.post("/api/authenticate", (req, res) => {
// do something
});
app.post("/api/accounts/create", (req, res) => {
// do something
});
app.post("/api/accounts/updateprofile", (req, res) => {
// do something
});
// more and more app.post
This approach makes the code difficult to read. Of course, we can solve these problems by making individual function modules. This is what I mean:
const express = require("express");
const doAuthenticate = require('../components/accounts/doAuthenticate');
const doCreate = require('../components/accounts/doCreate');
const doUpdateProfile = require('../components/accounts/doUpdateProfile');
const User = require("../../models/userModel");
app.post("/api/authenticate", doAuthenticate);
app.post("/api/accounts/create", doCreate);
app.post("/api/accounts/updateprofile", doUpdateProfile);
// more and more app.post
The point is that it is just a matter of taste. In my opinion, File-system routing creates a better development experience.
How is Next.js's File-system routing bad?
There is this one problem I do not like when it comes to File-system routing.
Imagine you have the following file structure:
src
|- models
| |-User.js
|-pages
|--api
|----accounts
| |----signup.js
|----this
| |---is
| |--an
| |---example
| |---endpoint.js
|--authentcate.js
Let's say each function requires User.js (which contains a mongoDB model).
In authenticate.js
, we would need:
User = require('../../models/User')
In signup.js
, it is:
User = require('../../../models/User')
It gets bad really fast when it comes to endpoint.js
, where it is:
User = require('../../../../../models/User')
Notices all the ../
, it may subjectively look bad in your code.
API Routes Overview
As I said above, you can create an API route by creating files under /pages/api/
.
An API Route looks like this:
export default (req, res) => {
// do everything your little heart desires
};
If you have previous experience with Express.js, you can easily understand this pattern. We notice two things:
- req refers to
NextApiRequest
which extends http.IncomingMessage - res refers to
NextApiResponse
which extends http.ServerResponse
Request
For http.IncomingMessage, see this. You can extract various data from req
, but it may be troublesome. To make it easier, Next.js offers several middlewares
which parse the req
.
req.cookies
- an object containing the cookies sent by the request. Defaults to {}req.query
- an object containing the query string. Defaults to {}req.body
- an object containing the body parsed by content-type, or null if no body is sent
Let's take req.cookies
for example. With req.cookies
, you can get the value of any cookies. For example, in my application, I may want to extract the value of req.cookies.sessionId
and use to authenticate the user with my server. NodeJs's http.IncomingMessage does not offer this shortcut. In a request, cookies are included in the header.
Node.js's Http class does not have any way to access the cookies directly. Still, it offers a way to access its header through req.headers. It should return something as an object such as below (refers to the image above:
{
'accept': 'application/json, text/plain, */*',
'cookie': 'sessionId=5d25db...;someothercookie=123456;coolcookie=abcxyz',
'host': 'localhost:3000'
'referer': 'http://localhost:3000/'
}
Notice how req.headers.cookie
will give me a string contains all the cookies. I will need to split them apart via a function like the one below:
const getCookies = (req) => {
var cookies = {};
req.headers && req.headers.cookie.split(';').forEach(function(cookie) {
var parts = cookie.match(/(.*?)=(.*)$/)
cookies[ parts[1].trim() ] = (parts[2] || '').trim();
});
return cookies;
};
See how having a middleware can save you a ton of work? With the middleware, I can access individual cookie directly by calling the abstraction req.cookies.theCookieIWant
.
Next.js API Routes goes further by providing body parsers by default (you can disable it). For example, this allows me to access the JSON in the request body as an object (without having to call JSON.parse()
).
Response
Similar to Express.js, Next.js provides me with several helpers. For example, I can send your response simply by calling res.send(yourResponse)
or set the status code by calling res.status(code)
.
Without the helpers, I will need to through tons of trouble like the one I describe for the req
.
To see all you can do with Node.js native's res
, visit the http.ServerResponse document. According to it, to send a response you need call res.write(yourResponse)
then res.end()
. To set a status code, simply set res.statusCode
to your desired value. To be honest, it is not so much a trouble if you make use of the native res.end([data][, encoding][, callback])
. Still, having the helper functions creates a better developing experience for me.
Below is a complete example of one of my function, getCurrentUser.js, which determine the current user based on his or her sessionId.
const User = require('../../models/userModel');
export default (req, res) => {
const sessionId = req.cookies.sessionId // Reading the sessionId from cookies.
User.findOne({ 'sessions.session': sessionId }) // One of sessions is sessionId
.then(user => {
if (!user) Promise.reject(); // No user with such sessionId (the sessionId may be invalid)
const { name, email, age, someinfo } = user;
// sending back the user info
res.send({
name,
email,
age,
someinfo
});
});
};
Dynamic Routing
One of the great things I like about Next.js 9's API Routes is its Dynamic Routing feature.
Imagine you have an endpoint to get the content of a blog post with the id 123:
GET /api/post/123
Or maybe you want an endpoint to delete that post:
DELETE /api/post/123
Either way, you will need a way to extract the postId (123
) parameter.
The way you do it is to create a file called [pid].js
. pid
is what you will refer to when you need to extract the parameter (not a placeholder, you actually name it pid
with brackets). It can also be anything: you can name it [postId].js
or whatever
Inside your code for [pid].js
, the postId will be accessible in req.query.pid
. In this case, pid
is what you name your file. If you name it [postId].js
, it will be req.query.postId
.
Below is an example:
// /page/api/post/[pid].js
export default (req, res) => {
const {
query: { pid }
} = req;
res.send(`Post: ${pid}`);
};
If you visit /api/post/123
. You will see "123" prints out on the screen. It is worth notice that this will accept any method (GET
, POST
, PUT
, DELETE
). You can filter it by making an if condition checking req.method
.
Below is a more practical example on a real DELETE /api/post/postId
endpoint. (remember that your implementation would be different)
// /page/api/post/[pid].js
import Post from '../../models/Post.js'
export default (req, res) => {
const {
query: { pid }
} = req;
if (req.method === "DELETE") {
// Find and remove the post with the speci
Post.findOneAndRemove({ postId: pid })
.then((post) => {
if (!post) return Promise.reject(new Error('cannot find post')); // no post found
// The post is deleted
res.send(`Post with the id ${pid} deleted`);
})
.catch(() => {
// catch the above Promise.reject()
res.send(`Cannot remove the post with the id ${pid}`);
});
}
};
Wrapping up
That's my first look into the new API Routes from Next.js v9. I have not finished transforming my codebase from using Express.js to using API Routes. Still, some considerations have to be made before I make the move. If I do, I will write a blog post on how I do so.
You can read Next.js 9's API Routes documentation here. If you have any explorations of this new feature, I would love to hear from you.