DEV Community

loading...

Express + NextJS - sample/tutorial integration

Alexey
Web++
Updated on ・4 min read

Context

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

Perhaps you have an existing Express API server and you wish to enable it to serve some front end with React/SSR. Or perhaps you want to write a chat application in NextJS, and you need to set up WebSockets. Maybe you just want to take control of the routing, or run some express middleware and fetch standard data for your pages before they are served.

This type of set up 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.

The integration

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

Let's take a look at some highlights.

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 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:

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

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

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

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

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

  init() {
    this.initSpecialPages()
    this.initDefaults()
  }

  initSpecialPages() {
    this.express.get('/my_special_page/:special_value', (req, res) => {
      const intValue = parseInt(req.params.special_value)
      if(intValue) {
        return this.next.render(req, res, `/special_int`, req.query)
      } else {
        return this.next.render(req, res, `/special_string`, req.query)
      }
    })
  }

  initDefaults() {
    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)
    })
  }
}
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.

I also added an example of a custom route that might not fit very well with NextJS's default routing. We support the /my_special_page/:special_value route, but if the :special_value is an integer, we render a special page that behaves quite differently from string-valued parameters.

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. I'll of course appreciate any actual attribution.

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. The server is set up to run in HTTPS in localhost, which I highly recommend - you can set up your certificates and environment following the project's README.

Other than that, the server should run with a simple yarn install, yarn start.

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.

I was trying to keep this project minimal, so I didn't include any custom CSS, or impose any sort of styling conventions - but you'll want to use either modular CSS or global CSS https://nextjs.org/docs/basic-features/built-in-css-support

Discussion (0)