DEV Community

Alexey
Alexey

Posted on • Updated on

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.

Top comments (8)

Collapse
 
mehran91z profile image
Mehran91z

Hi, thank you for this tutorial.

I developed your codes with 'http-proxy-middleware' and use /api/ patch to read from another express server URL.

But how I can render 404 error page of Nextjs instead of 404 response from Express?

proxy = {
        ["/api"]: {
          target: this.serverURL,
          changeOrigin: true,
          pathRewrite: {'^/api': '/',},
          onProxyRes: (proxyRes, req, res) => {
            if (proxyRes.statusCode === 404) {
              // REDIRECT WORKS as alternative way, but I don't want to do that.
              // res.redirect('/404');

              // THIS ONE NOT WORKING!
              next.render(req, res, "/_error", {});
          }
        }
}

const {createProxyMiddleware} = require('http-proxy-middleware');
Object.keys(proxy).forEach(function (context) {
   server.use(context, createProxyMiddleware(proxy[context]))
})
Enter fullscreen mode Exit fullscreen mode

Would you please help me on this?

Collapse
 
petercrackthecode profile image
petercrackthecode

Thank you so much for the article, Alexey! Quick question: can you explain more on the part where the code renders special_string and special_int based on the intValue? Why did we parse the intValue from the :special_value query?

Collapse
 
alexeydc profile image
Alexey • Edited

Hey Peter! Glad you liked the article :)

I added that as a random example of some custom logic that operates on routes - which is one advantage of using express together with NextJS. There's no special significance to parsing the int itself.

But thanks for pointing out the issue. I'll try to improve the article and code so hopefully the examples are more realistic.

Collapse
 
alexeydc profile image
Alexey

I updated the repository - hopefully the new examples make a bit more sense. Also updated the article to explain the examples better.

Collapse
 
evpy2 profile image
Ev-Py-2

I opened an issue on the Github repo regarding POST requests and how Express is handling JSON parsing...hoping to get some feedback on it!

Collapse
 
alexeydc profile image
Alexey • Edited

Oh awesome - thanks for opening it, I will respond tonight!

Collapse
 
evpy2 profile image
Ev-Py-2

I am trying to deploy this project into production, any insight on how to make this a smoother process would be great <3

Thread Thread
 
alexeydc profile image
Alexey • Edited

Happy to assist!

I usually prefer doing my own deploys on machines I can SSH to, vs hosting on heroku/netlify/Elastic BeanStalk/other managed black box solutions. It's usually cheaper, but IMO it's also more badass and gives deeper insight on computers. I like to start manual and automate as the project matures - but everyone's preference is different.

I prefer AWS, so I use EC2 machines with (any/latest) Ubuntu. IMO it's easier to set the manual deploy up than any automated solution.

This is what I do for deploys:
dev.to/alexeydc/quick-manual-deplo...

But then you also usually want https, this configuration works with that deploy style:
dev.to/alexeydc/modern-https-confi...