3

Express-like framework in Next.js

 3 years ago
source link: https://blog.usejournal.com/express-like-framework-in-next-js-27a884a0264d
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
1*2tmzU7bve-VlTkOMWsk_Hw.jpeg?q=20
express-like-framework-in-next-js-27a884a0264d

Express-like framework in Next.js

In this article, I demonstrate how you can craft an express-like API in Next.js without deploying an Express server with only a couple of adjustments.

If you’re looking for the benefits of Express’s framework but are happy with your application remaining serverless, this could be an option for you.

Considerations for using Next.js

I began my journey with Next.js because it makes server-side rendering for React straightforward. It allows me to easily choose what to render before page load, and has a pages folder for intuitive routing. I also discovered other benefits of going serverless like ease of deployment and focusing more on product development over managing servers.

Want to read this story later? Save it in Journal.

This means less work on your end, but also less control. It’s useful to read the limitations for the Vercel platform to see if it’s right for your application, taking note that there is a limit of 1000 concurrent requests without an enterprise plan.

Next.js’s API vs Expresss

A primary concern for me was how I would architect my API. With Serverless Functions, the ideal pattern is to have each route serve a single function in a single file in your pages/api directory (e.g. GET ‘/api/user/:id’ ).

I was migrating from an Express environment and enjoy the logical flow of leaving functionality to the controllers. My application had a custom authentication flow, so an endpoint like /api/login/ would be too large to comfortably restrict to a single function. Not to mention that I want to reuse some of this functionality, so having this logic stored in isolated endpoints did not make sense to me.

The question became:

Can I simply use Express with Next.js?

Yes, but not without some tradeoffs.

One option is to set up a custom server, but this defeats some of the reasons to use Next.js in the first place, as you cannot deploy on Vercel and you’re no longer serverless. However, this is the right option for many applications, especially larger ones that benefit from managing their own server.

Another option is to use Express within a serverless function. Vercel recommends this only as a means to migrate your app and essentially ween yourself off of a dedicated server. Personally, I think it’s a bit silly to run Express in a serverless environment. It also means you are executing a full server implementation on each request, which violates the pattern of serverless functions serving one purpose. But I’m not suggesting this can’t work for you.

I decided to stay within the bumpers of Next.js and try to get the benefits I wanted out of Express’s framework without using Express.

The problem I ran into

I was set on having a controller directory in my app where I export a myriad of functions and call them like middleware from my serverless functions.

This is cumbersome to do in Next.js however, because it lacks a next() function that Express uses to escape the middleware chain and bypass the rest of the functionality.

Essentially, not having access to next() means I cannot easily break out of middleware early.

What NOT to do:

In this example, we have an endpoint that awaits the verification of a token before sending user data. If the middleware function tells us we are not verified, we need to handle that response and avoid executing the next middleware function.

/api/authenticateUserimport authController from 'controllers/authController';export default async (req, res) => {try {  // Controller sends back a boolean "verified" and "userId"
const result = await authController.verifyToken(req, res);
if (result.verified === false) {
return res.json({ error: 'Not authenticated'})
}; const payload = { userId: result.userId }; // Fetch user data by userId. Returns username
const data = await authController.header(req, res, payload);
const { username } = data; return res.json({ username });} catch(e) {
return res.status(500).send(e.message);
}};

As you can see, this looks nothing like the clean middleware chain we’re used to in Express. I have to pass information like { verified: true } back to the “awaiting” function in order to handle exceptions.

In express, we would just return something like next(err) or res.json({error: ‘custom error’ }) inside the controller, where, if we don’t hit the next() statement, we do not proceed. Here, if we call res.json() from the controller, it will return out of the function, but not end the response all together.

Also notice how clumsy it is not to have a res.locals object to pass data between. We have to pack data into a payload and unpack the results of every controller call.

The Solution:

(explanation provided below)

import wrapper from ‘utils/wrapper’;
import authController from ‘controllers/authController’;const handler = async (req, res, middlewareChain) => { await middlewareChain
(
authController.verifyToken,
authController.getUserId,
)
.then(result => { if (!result) return; })
.catch(e => { res.status(500).send(e.message); }) return res.json({
username: res.locals.username
});};export default wrapper(handler);

This doesn’t explain much without showing you the wrapper function I import at the top. Just notice the fact that we are not exporting the function, but the wrapper with the function passed in.

The Wrapper:

Wrapping the function allows us to execute our function with modified res and req functions, as well as add any other functions to our parameters (like middlewareChain).

Here’s what the wrapper file looks like:

utils/wrapperconst wrapper = handler => {return (req, res, middlewareChain) => {

/* res.locals object */
res.locals = {}; /* middlewareChain function */
middlewareChain = async (...funcs) => {
let finished = true;
for (const func of funcs) {
await func(req, res)
if (res.finished) {
finished = false;
break;
};
};
return next;
}; return handler(req, res, middlewareChain);
};
};module.exports = wrapper;

First, you’ll see we can declare and utilize res.locals in the same exact way we would with Express. This allows us to easily pass data between functions.

As for the middlewareChain function, this takes all of your controller functions as arguments. For each function, we await the response and check if we called res.json() or res.send() somewhere along the way. We check this by utilizing the res.finished property, native to the res object. Its value changes from true to false when you call res.json() or res.send(). (This is normally meant to ensure we don’t set the response twice ).

If I want to exit my controller function early, I can call return res.json({data}). My middlewareChain will catch that I want to return a response and break out of the loop. In addition to calling break, we also set a finished variable to false, making middlewareChain’s return value a boolean. We handle this response in .then(), saying if it evaluates false, return.

Of course, another way to break out of controller functions is to throw an error, but this is not appropriate for every situation.

Other things you can do with a wrapper:

In my application, I also have error handing and cookie handling functions (res.cookie) that I place on the response object, which I highly recommend. But I’ll keep this one simple. Point is, you can attach any sort of function or object you want onto req and res that makes your life easier or takes out repetitive code.

In conclusion

This strategy could help you keep your Express-like workflow without creating a custom server and without sacrificing the ability to use controllers that can halt the “middleware” chain.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK