Express + NextJS - sample/tutorial integration

alexeydc

Alexey

Posted on June 20, 2021

Express + NextJS - sample/tutorial integration

Context

While NextJS is a wonderful tool in its own right, augmenting it with Express makes for a powerful combo.

One motivation may be simplicity - if you have a project you're trying to prototype and rapidly iterate on. These days, it's common to host the front end separately from the API, but then your project starts off as a distributed system - and you have to deal with extra complexity up front.

Some other use cases where it makes sense to do this type of combination:

  • Enabling an existing Express API server to serve some front end with React/SSR
  • Run some express middleware and fetch standard data for NextJS pages before they are served
  • Adding custom logic to NextJS routing
  • Adding WebSocket functionality (e.g. for a chat app)

This type of setup is documented in NextJS itself: https://nextjs.org/docs/advanced-features/custom-server

In the standard example, they use Node's http package; we'll use Express to take advantage of its middleware and routing capabilities.

Source code

I've provided an example barebones integration - as a github template - at https://github.com/alexey-dc/nextjs_express_template

I also wrote an article on how to make this type of setup production-ready with PM2: https://dev.to/alexeydc/pm2-express-nextjs-with-github-source-zero-downtime-deploys-n71

Using that setup, I've hosted the demo at https://nextjs-express.alexey-dc.com/ (it's just the template run on a public URL). The main difference with the code explained here is the PM2 configuration, which I use for zero downtime deploys.

The integration

Let's take a look at some highlights of this NextJS+Express setup.

The main entry point is index.js, which sets up the environment and delegates spinning up the server:

require("dotenv").config()
const Server = require("./app/server")
const begin = async () => {
  await new Server(process.env.EXPRESS_PORT).start()
  console.log(`Server running in --- ${process.env.NODE_ENV} --- on port ${process.env.EXPRESS_PORT}`)
}
begin()
Enter fullscreen mode Exit fullscreen mode

Note that I'm relying on dotenv to load environment variables - e.g. EXPRESS_PORT, NODE_ENV, and a few others. You can see the full list of necessary environment variables in the README in the github repository.

In the server, both nextjs and express are initialzed, along with express midleware and a custom NextjsExpressRouter I built to take the routing over from NextJS into our own hands:

  this.express = express()
  this.next = next({ dev: process.env.NODE_ENV !== 'production' })
  this.middleware = new Middleware(this.express)
  this.router = new NextjsExpressRouter(this.express, this.next)
Enter fullscreen mode Exit fullscreen mode

The middleware I included is quite barebones, but serves as an example of what you might have in a real application:

  this.express.use(bodyParser.json());
  this.express.use(bodyParser.urlencoded({ extended: false }));
  this.express.use(favicon(path.join(__dirname, '..', 'public', 'favicon.png')));
Enter fullscreen mode Exit fullscreen mode

The NextjsExpressRouter is really the heart of the integration. Let's take a closer look.

NextjsExpressRouter

The idea is to allow GET routes for pages to co-exist with API HTTP routes:

class NextjsExpressRouter {
  constructor(express, next) {
    this.express = express
    this.next = next
  }

  async init() {
    this.initApi()
    this.initPages()
    this.initErrors()
  }

  initApi() {
    return (new (require("./routes/api.js"))(this.express)).init()
  }

  initPages() {
    return (new (require("./routes/pages.js"))(this.express, this.next)).init()
  }
// ...
/* Some standard error handling is also included in the repo code */
}
Enter fullscreen mode Exit fullscreen mode

I split out the API from the page routes into separate files, and I find that as the codebase grows, it helps to impose some sort of grouping or hierarchy on endpoints. Pages and API calls seem like the most basic organization. Note I made the init() function async. In this case we don't need to run any I/O operations or other async initialization, but in the general case we might want to.

For my larger projects, the API typically itself has several sub-groups, and sometimes pages do as well. In this sample project that has very few routes, the API and pages are a flat list of routes:

const data = require("../data/integer_memory_store.js")

class Api {
  constructor(express) {
    this.express = express
  }

  init() {
    this.express.get("/api/get", (req, res) => {
      res.send({  i: data.value })
    })

    this.express.post("/api/increment", (req, res) => {
      data.incr()
      res.send({ i: data.value })
    })
  }
}
Enter fullscreen mode Exit fullscreen mode

Obviously this is just a minimal sample API - all it does is allows you to read and increment an integer stored in memory on the server.

Here's how the NextJS page routes are defined:

const data = require("../data/integer_memory_store.js")

class Pages {
  constructor(express, next) {
    this.express = express
    this.next = next
  }

  init() {
    this.initCustomPages()
    this.initDefaultPages()
  }

  initCustomPages() {
    /* With a monolith api+frontend, it's possible to serve pages with preloaded data */
    this.express.get('/preload_data', (req, res) => {
      res.pageParams = {
        value: data.value
      }
      return this.next.render(req, res, `/preload_data`)
    })

    /* Special-purpose routing example */
    this.express.get('/large_or_small/:special_value', (req, res) => {
      const intValue = parseInt(req.params.special_value)
      if(isNaN(intValue)) {
        return this.next.render(req, res, `/invalid_value`, req.query)
      }
      if(intValue < 5) {
        return this.next.render(req, res, `/special_small`, req.query)
      } else {
        return this.next.render(req, res, `/special_large`, req.query)
      }
    })
  }

  initDefaultPages() {
    this.express.get('/', (req, res) => {
      return this.next.render(req, res, `/main`, req.query)
    })

    this.express.get('*', (req, res) => {
      return this.next.render(req, res, `${req.path}`, req.query)
    })
  }
}

module.exports = Pages
Enter fullscreen mode Exit fullscreen mode

The page routes showcase setting up a root / path, and a fallback * path - if we're not able to match the GET request, we default to what NextJS's standard behavior is: rendering pages by filename from the /pages directory. This allows for a gentle extension of NextJS's built-in capabilities.

There are 2 examples for custom routing.

In the first example, we pre-load some data, and bake it into the page before serving it to the user. This may be useful to avoid an extra HTTP roundtrip after the page renders, and is difficult to pull off w/o a monolithic API+frontend setup as presented here.

In the second example, we render different variants of a page depending on an integer value in the route - or an error, if the input is invalid. Perhaps a real application may fetch user data, and render it differently depending on some condition (e.g. the viewer's relationship with them) - and render an error if the user is not found.

Using the template

I licensed the code under MIT - which means you are free to use the template in closed-source and commercial products, and make any modifications you want. Please attribute/give credit if you are able to!

It's also a template on github, which means you can just click a button and start a new repo based on https://github.com/alexey-dc/nextjs_express_template

Screen Shot 2021-06-20 at 5.36.59 PM

Running

The instructions for running are in the github repo.

Iterating

You'll probably want to delete the sample custom endpoint and associated pages I provided - and start replacing them with your own!

I included a sample organization for pages as well - the page roots are in pages as nextjs mandates, but all the reusable jsx is in views - for the demo, I was using a common layout for pages, and the Layout component is housed in views.

💖 💪 🙅 🚩
alexeydc
Alexey

Posted on June 20, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related