Migrating from a custom server to native Next.js API routes

At Bueno we deploy our applications to GCP via Pulumi and Kubernetes (need more buzzwords?). When we first started building our applications with Next.js we knew we needed to proxy any API requests to our existing Spring + Kotlin backend service and a way to check if the apps are healthy in our k8s clusters.

Historically this could be done with either a custom server and some middleware or Next.js rewrites. Recently we've been able to completely remove the custom server while retaining an API proxy and support for a k8s readiness probe.

How we did it before

At the time we started building our first Next.js app, Next.js rewrites didn't exist so we went down the complete custom server route with:

  1. a custom express server with express-actuator and express-http-proxy middleware
  2. nodemon and a second tsconfig.server.json file to manage the compilation of our custom server for development
  3. Additional build scripts and Dockerfile steps to package up the custom server

The combination of which ended up making our application far more complex to maintain than we'd like, slower to start, and slightly more difficult to package up for k8s.

How we do it now

Without a custom server of course!

The proxy

For the proxy we've made use of the widely used package node-http-proxy and a custom Next.js API route at /api which forwards all requests to our proxy target configured by a process.env.API_HOST variable.

A previous version of this post suggested using the next-http-proxy-middleware package which we had to replace due to a serious memory leak issue resulting in server restarts. The following achieves the same functionality without the memory leak.

Install the proxy library:

npm install --save http-proxy

Then create a catch-all route at /api:

./src/pages/api/[...proxy].ts
import httpProxy from 'http-proxy'
import { type NextApiHandler } from 'next'

// We add support for path rewrites by extending the default options
interface Config extends httpProxy.ServerOptions {
  rewritePath: {
    pattern: string
    replace: string
  }
}

// First we create a function that creates our handler and proxy instance
function setupProxy({ rewritePath, ...config }: Config): NextApiHandler {
  const proxy = httpProxy.createProxy(config)

  return (req, res) => {
    // here we replace the URL that's passed through to the proxy web handler
    // with the rewrite that we've defined
    const exp = new RegExp(rewritePath.pattern)
    req.url = req.url?.replace(exp, rewritePath.replace)

    proxy.web(
      req,
      res,
      {
        changeOrigin: true,
        ...config
      },
      // NOTE: You _must_ handle any errors here as the http-proxy package does
      // not handle them for you. This will not affect any downstream severs
      // returning status codes >=400, only errors communicating with downstream
      // servers are handled here.
      // We've found a 500 code with a simple message here adequate.
      error => {
        console.error(error)
        res.status(500)
        res.send(error.message)
      }
    )
  }
}

// Then we call to configure it and create the function which is exported as normal
export default setupProxy({
  target: 'http://my.api.url:1337',
  rewritePath: {
    pattern: '^/api',
    replace: ''
  }
})

The externalResolver: true config property here is to prevent Next.js from warning us about possible stalled requests. Next has some internal checks to ensure that any /api route returns some data. We're effectively bypassing this completely by using a proxy server and Next.js is (correctly) freaking out about it.

Without this config Next will warn you about potentially stalled requests. See this related GitHub Issue

A note on Next.js rewrites

At the time of writing the rewrites property in Next.js does not support runtime configuration via process.env variables. This made it a non-starter for us as we need to be able to configure our proxy target URL well after the app is built and deployed to a cluster.

The actuator

Because we use Pulumi and K8S to orchestrate our infrastructure we need a way to determine if our apps are up (via readiness probes). Originally we used the express-actuator middleware in the custom server from earlier to provide some Spring-style actuator endpoints. In order to remove the custom server we need to write those same endpoints but this time with native Next.js API routes.

We've open soured @bueno-systems/next-actuator to do exactly that for you!

Install it with:

npm install --save @bueno-systems/next-actuator
# or
yarn add @bueno-systems/next-actuator

Then in your Next.js application it's as simple as:

./src/pages/api/actuator/[...actuator].ts
import nextActuator from '@bueno-systems/next-actuator'

export default nextActuator()

This will set up api routes for:

  • GET /api/actuator/health
  • GET /api/actuator/info
  • GET /api/actuator/metrics

For more information on usage, responses, and possible configuration, see the code on GitHub.

Closing thoughts

Not only did we remove more dependencies than we added when we originally built this functionality into our app we also saw gains in developer experience by:

  • Reduced complexity in packaging and deployment
  • Reducing dev mode boot times
  • Allowing our devs to create custom API endpoints or mock APIs in the Next.js way without extra dependencies either in code or on our backend team

We (as a company) have also released our first open source library that is truly useful that I'll personally be using in my own Next.js projects in the future.

🎧 Nothing playingShow history