Hacking | 404 Nuxtjs

Hacking | 404 Nuxtjs
Hacking Nuxt's 404 logic for maximum awesome (and easy proxying)!. No no, not what it renders on 404. What it does on 404.

Here at FINN small jobs, we’re huge fans of Vue.js. So, when we started reading about Nuxt.js, we got really excited, and quickly added it to our stack. In order to keep our React and JSP pages alive, however, we had to find a way to make it proxy to those pages when they’re requested. So, we tore Nuxt down, took away its 404 rendering, and injected our own logic to proxy when there’s nothing to render. Here’s how you can too!

Wait, what the heck is Nuxt?!

Nuxt is a framework built on top of Vue that handles both the client-side and server-side of things. It reduces boilerplate, has server-side rendering out of the box, and lots of other great things. It helps you to get started using Vue without having to worry too much about config, and has best practices included.

Why would I want to override Nuxt’s behaviour?

Well, in our case it stemmed from wanting to proxy to our old app when requests come through that our Nuxt app doesn’t have a view for. This achieves a few things

  • We don’t have to move our entire app in one go
  • We don’t have to manually write rules for every view in our legacy app into our proxy
  • We still get all the advantages of Nuxt and server-side rendering

How does Nuxt handle requests?

Before we can talk about how to hack it, we need to understand how Nuxt handles requests and errors. Under the hood, Nuxt acts mostly like a normal Connect style Node.js server; all the logic is built on a set of middleware functions, including the routing of requests.

In Nuxt the way you add a middleware is by creating a middleware function, then wiring it up in your nuxt.config.js file

nuxt.config.js

// nuxt.config.js
const pkg = require('./package')

module.exports = {
    ...
    serverMiddleware: [
        '~serverMiddleware/override404.js'
    ],
    ...
}

override404.js

// serverMiddleware/Override404.js
export default (req, res, next) => {
    console.log("Hi Medium! This log will print for all requests")
}

Note, these middleware are Connect style, not Express style. This means that you don’t have access to all the convenience methods from Express on req and res

So far so good! Now we have a middleware that runs on all requests. Now how do we override the logic for 404?

Those of you who work a lot with Connect/Express apps might guess that you can inject a regular Connect error handling middleware (a regular middleware, just with the signature (err, req, res, next) ) however Nuxt doesn’t treat 404 like an error. That means that your error handler never sees that there’s an issue.

This is where we wander into undocumented territory of Nuxt. Extending what Nuxt renders on 404 is easily achieved. Changing its behaviour is not so.

Breakdown of the problem

  • Nuxt doesn’t give middlewares an easy way to see if a route is valid or not
  • Nuxt runs its own 404 handler that can’t be intercepted in the middleware chain
  • Nuxt doesn’t have a clearly documented way of overriding the 404 handler on the server side

What we need to do

  • Intercept all traffic and check if the requests are renderable by Nuxt
  • Proxy all other requests

How we did it

The first step is to figure out which routes are renderable by Nuxt. Fortunately, Nuxt’s builder hooks and modules are really helpful here. We can write a module that writes all valid routes to file at build time!

nuxt.config.js

// nuxt.config.js
...
modules: ['~/modules/writeRouteToFile.js']
...

writeRoutesToFile.js

// modules/writeRoutesToFile.js
import fs from 'fs'

export default function writeRouteToFile() => {
    const routesFile = 'nuxtPageRoutes.json';
    const fileExists = fs.existsSync(routesFile)
    if(!fileExists) {
        // Middlewares are evaluated during build even though requests aren't flowing through them,
        // so we have to ensure the file exists otherwise override404 will throw from not finding the file
        fs.writeFileSync(
            routesFile,
            JSON.stringify([])
        )
    }
    this.nuxt.hook('build:extendRoutes', routes => {
        // routes here is an array of nuxts internal route objects.
        // On routes[x].path we find paths like '/', '/posts' etc and this is the information we're after
        
        fs.writeFile(
            routesFile,
            JSON.stringify(
                routes.map(route => route.path),
                { encoding: 'utf-8', flag: 'a+' }, // The flag a+ creates the file if it doesn't exist
                err => {
                    if (err) throw err
                }
            )
        )
    }
}

Now every time the app is built, our module saves all the route paths as an array to a file called nuxtPageRoutes.json

This means that we can make the routes available to our middleware. One step closer!

Next, we have to take these path-literals and make them into regexes. Luckily for us, we can use the library that Connect and Express uses under the hood, path-to-regexp! Run yarn add path-to-regexp or npm install path-to-regexp to add it to your project.

Now that we have that in place, let’s return to our override404.js middleware and make it hijack the 404 logic!

override404.js

import fs from 'fs'
import pathToRegexp from 'path-to-regexp'
import ProxyMiddleware from 'http-proxy-middleware'

const NODE_ENV = process.env.NODE_ENV ? process.env.NODE_ENV.toLoverCase() || 'development'

// Take all routes and create path regexps from them. This means that we account for requests like /posts/1 and not just /posts
const readRoutesFromFile = () => JSON
    .parse(fs.readFileSync('nuxtPageRoutes.json'))
    .map(pathToRegexp)

let nuxtPageRoutes = readRoutesFromFile()
const proxy = ProxyMiddleware({ target: ... }) // target is set to the location of our legacy app
export default (req, res, next) => {
    if (NODE_ENV !== 'production') {
        // While in development mode, the available routes might change, so we need to check them every time.
        nuxtPageRoutes = readRoutesFromFile()
    }
    
    if(nuxtPageRoutes.some(route => route.exec(req.url)) {
        // A valid path was found, so we let Nuxt render it!
        next()
    } else {
        // Here we invoke our proxy middleware, but really you can put whatever it is you want here!
        proxy(req, res, next)
    }
}

And there it is! 🎉 We now have a fully functioning proxy that intelligently proxies our traffic, renders our new app with SSR, and lets us migrate in phases with minimal disruption to our other work.

tl;dr: write a Nuxt module that writes all routes to a file on build, read those routes in a serverMiddleware, convert them to regexes with pathToRegexp , check incoming requests against those regexes, if there’s a match pass it further down the middleware chain, if there’s no match run your own function.

Suggest:

Nuxt.js - Practical NuxtJS

Is Vue.js 3.0 Breaking Vue? Vue 3.0 Preview!

Learn Vue.js from scratch 2018

18 Nuxt JS beginner tutorial - Start weather app project

Vue.js Tutorial: Zero to Sixty

Learn Vue.js from Scratch - Full Course for Beginners