NextJS, Express, Typescript, etc. in a monorepo setup

While building a pet-project that uses nextjs, I realized that I wanted to have a fully separated monorepo package setup for nextjs and my go-to nodejs API framework express while also making express the custom server for nextjs instead of the built-in server that NextJS is bundled with.

TL;DR: Check out the code here

Before reinventing the wheel I spent quite some time looking for someone that had already done this but I found nothing, if you know of something similar leave it in the comments section so I can compare my solution to theirs.

The closest thing I knew about was an official example for setting up a custom-server in nextjs. With this example at hand then the most important thing was to move the server code to its own package and making sure it worked well for both nextjs and my own API endpoints.

The objective of this post is not to explain how to setup a typescript monorepo with lerna and yarn. For an in-depth explanation, you can check https://medium.com/@NiGhTTraX/how-to-set-up-a-typescript-monorepo-with-lerna-c6acda7d4559

Overall in this example, I set up three packages:

  • web-client

Contains all the nextjs pages, components for the client-side work

  • web-server

Contains all the express code for serving the API endpoints that your project might need and also to handle nextjs requests.

  • web-server-logger

Contains an example package, a logger with bunyan to be used in the web-server package.

The key things I had to realize after lots of trial and error was the way nextjs works with custom servers where the main code to explain is:

web-server nextjs handler

import { Router, Request, Response } from 'express';
import URL from 'url';
import nextapp from '@jcalderonzumba/web-client';

const router = Router();
const handler = nextapp.getRequestHandler();

// TODO: FIX deprecation warning. (NextJS requires the query string parameter)
router.get('/*', (req: Request, res: Response) => {
// eslint-disable-next-line
return handler(req, res, { ...URL.parse(req.url, true) });
});

export default router;

web-server starter

import { logger } from '@jcalderonzumba/web-server-logger';
import express from 'express';
import cookiesMiddleware from 'universal-cookie-express';
import bodyParser from 'body-parser';
import compression from 'compression';
import cookieParser from 'cookie-parser';
import * as routes from './routes';
import nextapp from '@jcalderonzumba/web-client';

const port = process.env.PORT ? parseInt(process.env.PORT, 10) : 3000;

const app = express();
app.set('x-powered-by', false);
app.use(compression());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(cookieParser());
app.use(cookiesMiddleware());
app.use('/api', routes.serverAPIRouter);
app.use('/', routes.nextJSRouter);

(async (): Promise<void> => {
try {
await nextapp.prepare();
app.listen(port, listenPortError => {
if (listenPortError) {
logger.error(
{ err: listenPortError },
`[ExpressServer] Error starting server ${listenPortError}`
);
throw listenPortError;
}
logger.info(`[ExpressServer] Server ready, listening on port ${port}`);
});
} catch (err) {
logger.error(
{ err },
'[ExpressServer] Unexpected error while starting server'
);
}
})();

In order for express to be the custom server for nextjs, we need to connect express js routes with nextjs request handler and we also need to prepare nextjs before starting the server.

In both files, we make use of a “nextapp” that lives in a different package. This is the key issue I found while migrating from the custom-server example to a monorepo so I had to create a module withing the web-client package that could be imported in web-server.

web-client nextapp module

// file index.ts
import next from 'next';
import { resolve } from 'path';

const dev = process.env.NODE_ENV !== 'production';
let nextDir = __dirname;
if (nextDir.indexOf('/dist') !== -1) {
nextDir = resolve(__dirname, '../');
}

const nextapp = next({
dev,
dir: nextDir
});

export default nextapp;

To be able to compile both this single-file module (index.ts) and also nextjs I had to modify nextjs default build dir and also create a specific tsconfig file for said module

next.config.js

module.exports = {
poweredByHeader: false,
distDir: 'dist/next'
};

tsconfig.app.json

{
"extends": "../../tsconfig.build.json",
"compilerOptions": {
"noEmit": false,
"declaration": true,
"outDir": "./dist",
"rootDir": "./src",
"baseUrl": "./"
},
"include": ["src/index.ts"]
}

With this configuration, nextjs stuff will be compiled to dist/next/** and the single file nextapp module will land in dist/index.js.

I had to create this structure because in development mode nextjs deletes the build dir so it will also kill index.js nextapp module and that can be troublesome for the web-server as it depends on dist/index.js.

Once you do these changes you will have a successfully connected express-server and nextjs where all your requests will be handled by express and if needed forwarded to nextjs handlers that live in a different package.

Why did I do all of this?

Challenge and learning

I am a Director of Engineering at Travelperk, (we are hiring engineers, join us), which means my work time needs to be spent on other things than coding, however, I still enjoy coding especially with nodejs/react/nextjs/typescript, therefore when I failed to find a solution for what I wanted to do in my pet-project I knew it was a “challenge accepted” situation.

Having said that, I have not coded professionally for over eight years so if my approach is not what you would have done, leave your suggestions in the comments section.

Separation of concerns

This setup allows me to separate concerns of client-side code and server-side code, so I can have a package that only cares for my nextjs pages, components, styles, CSS, etc. and another package that host the endpoints I might need to consume, middlewares, etc.

Security on private pages.

I’m not saying nextjs is not secure or that it can not be secured but with this setup, to add an additional layer of protection of private page I can just add a middleware in express that forces a user to be logged in to access that content and since nextjs is SSR, every time someone not authenticated tries to access that page, express will stop it before it even reaches nextjs.

Cleaner package.json

I have built pet-projects using the traditional nextjs custom-server approach of everything in the same package and that ends up with lots of libraries that are only needed for the server, once again, you are mixing stuff that should be separated.

Director of Engineering at Travelperk

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store