Nuxt: The Hard Parts

In this article I'll explore one of the main sources of confusion and help requests in the Nuxt community: the @nuxtjs/auth module. The reason this module causes so much confusion is that it requires users to understand a few core concepts of Nuxt that are often ignored by new developers eager to get something working quickly. I realized these concepts constitute what one might call the hard parts of Nuxt.

First I'll dive into the features that allow the @nuxtjs/auth module to work, as a way to explore Nuxt's advanced features. Then I'll demonstrate how to build a simple auth system.

That being said, I won't discuss the particulars of any authentication scheme, I'm assuming you already know that.

Instead, this article focuses on the tools and the confusion surrounding the tools Nuxt offers to effectively use and implement APIs, and also control routing.

Vuex recap

First off, you need to understand Vuex. The linked article by Flavio Copes is a great introduction, but, in a nutshell:

  • You declare all of your application data as a state object. By predefining all of the state's structure (including empty properties as null), the Vuex engine can track and react to changes to them. A basic state within store/index.js might look like this:
export const state = () => ({
  user: {
    authenticated: false,
    id: null
  }
})
  • You create mutations (which are just a set of functions) to actually perform state changes, i.e., you can only assign new values to state properties inside a mutation handler.
export const mutations = {
  authUser(state, user) {
    state.user.id = user.id
    state.user.authenticated = true
  }
}
  • With that, the following becomes available in Vue.js components:
this.$store.commit('authUser', { id: 123 })
  • To alter the state, you don't actually need actions. Most of time you'll find yourself doing random $store.commit() calls to update your application state. Mutations however don't allow you to run asynchronous code while performing state changes. For that you can use an action, which also allows you to call multiple other mutations. Note that you can't directly assign new data to state via an action other than by calling a mutation. Protip: use unholy to turn $store.commit() into a generic recursive merge function.
  • Once a mutation is executed, any piece of a Vue template tied to the changed properties (via mapState) is automatically updated.
<template>
  <div class="auth-menu">
    <span
      v-if="user.authenticated">
      Logged in
    </span>
    <a
      v-else
      href="/login">
      Login
    </a>
  </div>
</template>

<script>
import { mapState } from 'vuex'

export default {
  computed: mapState(['user'])
}
</script>

In Nuxt, simply creating a file at store/index.js is enough for $store to be injected in your Vue application. The fact that Nuxt relies so heavily on the filesystem to determine aspects of the application can be confusing at first, but extremely convenient in practice.

SPA vs SSR confusion

Nuxt is a hybrid Vue.js framework. It lets you build both a regular Vue.js SPA (Single Page Application), that renders solely on the browser, and a server-side (pre)rendered (SSR) Vue.js SPA. This is still not fully understood by a lot of people, perhaps due to the SSR misnomer. Applications are still actively rendered on the client via the Vue engine. That means $router.push() on the client side won't trigger any network calls, as expected. But if you access a /route directly, the first time it comes back from the server it'll come prerendered, ready for display. It's important to fully understand this basic concept in order to grasp all associated aspects.

Nuxt modules vs plugins confusion

It's useful to understand how Nuxt modules work, so you know what's going on or at least where to look for things. Nuxt modules are just functions that add more code to your Vue.js app. By understanding how they're used, you can even easily dismember and adapt them into your own code if you ever need to heavily customize something.

Nuxt is a runtime with a built-in compiler. What it does is take your code, load it and wrap it within a predefined Vue.js app, with all heavy lifting done for you.

Nuxt will create a VueRouter instance, a Vuex store, Webpack configuration etc, so you can focus on your Vue application code.

The Nuxt Module API provides hooks that let you run code before or after the actual build, among others.

It also provides functions to add new code to the final compiled Vue.js app, such as addTemplate() and addPlugin().

Plugins are just code Nuxt runs on top of your entire application. You use plugins to add extensions to your Nuxt app, such as Vue.js plugins. To enable a plugin, add it to the plugins configuration property in nuxt.config.js. Plugins can run on the server, client or both. Use the .client.js and .server.js suffixes or none to indicate both.

If you look into @nuxtjs/auth's' source code, you'll see a addPlugin() call in lib/module/index.js to add auth/plugin.js to your Nuxt app. It will cause that code to run both on the server for every request (before SSR) and also on the client during initialization.

Middleware confusion

Plugins are a way to run code on top of your application, and even access the Vuex store, as used by @nuxtjs/http for defining request and response interceptors, but you can't make routing decisions with plugins. For that you need a router middleware. A router middleware is a magic piece of functionality in Nuxt, because it will work locally for navigation but will also run on the server for first requests.

Think of it as a streamlined version of VueRouter's navigation guards. With Nuxt, you can simply set the router.middleware property in nuxt.config.js for global navigation guards, or use the middleware property in individual Nuxt pages or layouts.

Nuxt will store all files you place under middleware/ in an internal object, so middleware can be accessed and defined by a key. The default application Nuxt generates will include a middleware.js file in its final code, where that object is exported. That is also a way for new route middleware to be added to a project by a module.

The @nuxtjs/auth module does exactly this: it'll add a middleware function named auth to the object exported by middleware.js (the file that is automatically generated by Nuxt). That's why there's a seemingly cryptic middleware.js import on line 3 of lib/module/index.js. When the plugin added by @nuxtjs/auth runs, it'll also make the auth middleware available for you to enable globally or directly on pages and layouts.

Nuxt has an entirely different type of middleware: serverMiddleware. This is an array where you can list functions that are treated as connect middleware. The @nuxtjs/auth module adds one such serverMiddleware in its oauth provider.

Nuxt serverMiddleware are ran serially, only on the server.

Knowing how a module works, what operations it can perform in your project code, and also, how plugins and router middleware differ, should be enough to get you on a path of debugging any problems with the @nuxtjs/auth module. The code for the Auth class, which is instantiated and injected in your Nuxt app as $auth, is rather sophisticated, but that sophiscation allows you to reuse multiple authentication providers with ease.

Build your own

The @nuxtjs/auth module is great and likely the best choice if you need to support multiple authentication methods the fastest way possible. But once you become familiar with Nuxt you'll probably also consider rolling out everything your own way to meet other requirements. I've created a simple boilerplate to help you get started. I call it quickjam, where jam refers to the new JavaScript, APIs and Markup proposed acronym.

See it here: https://github.com/galvez/quickjam

You can use serverMiddleware to add an API on top of your Nuxt.js API. That means all requests will reach the same server, but you can make requests that start with /api/ land in a special request handler, whereas all other requests will reach the Nuxt internal middleware as usual. This is far from an ideal architecture, as with this you effectively have two different applications running with the same process, which makes it harder to isolate and scale separately. If scalability is not an immediate concern, it has shown to work surprisingly well in a number of projects with a properly load balanced pool of Nuxt server processes (usually behind nginx).

This commit adds a minimal Nuxt boilerplate: package.json, nuxt.config.js, middleware/auth.js, store/index.js, api.js and eslint configuration. Yes, it is unfortunate that I need to add that many packages to devDependencies in order for this to work. Hopefully this will get better once eslint-plugin-nuxt is finalized.

While we're on the topic of eslint, this eslint-plugin-vue rule is incredibly annoying. To me it's a matter of personal preference: running eslint --fix with this rule produced code just flat out bizarre to me so I've disabled it with this commit.

Notice how I import api.js as serverMiddleware in nuxt.config.js so I can easily add it to the Nuxt configuration object. The api.js file exports an Array, where we'll add some server API functions next.

Please be aware that this boilerplate has practically zero error handling. It's merely a didactic example of how to use APIs, server middleware and routing middleware in Nuxt. Make sure you plug all the holes if adapting it into a real app.

The @nuxt/http module

But first, let's make sure our Nuxt app can actually perform API requests. For a long time that would have been a job for @nuxtjs/axios module, which provides an integrated axios instance. Now, @nuxtjs/axios can be replaced by an improved alternative: the @nuxt/http module, which relies on ky instead of axios. It has a slightly different API but significantly lower bundle size.

Here's the updated nuxt.config.js:

import serverMiddleware from './api'

export default {
  serverMiddleware,
  modules: ['@nuxt/http'],
  server: {
    port: 3000
  },
  http: {
    baseURL: 'http://localhost:3000'
  }
}

Since the API's base URL will be the same as the Nuxt's app, setting baseURL isn't really necessary, but in practice you'll want to explicitly set it in order to be able to set it with an environment variable in the future. Same goes for Nuxt's default server port, which is 3000.

To help you get a taste of the @nuxt/http module, I added a simple server middleware function for /api/ping which returns a timestamped pong. You can see it all in this commit.

export default [
  {
    path: '/api/ping',
    handler(req, res, next) {
      res.end(`API pong at ${new Date().getTime()}`)
    }
  }
]

The asyncData() method in index.vue will make a HTTP request no matter how the page is accessed (locally via $router.push() or a first request to the server). In the case of a first request, the HTTP request will be performed seamlessly server-side.

async asyncData({ app }) {
  const response = await app.$http.get('api/ping')
  return {
    pongMessage: await response.text()
  }
},
mounted() {
  alert(this.pongMessage)
}

JSON support

Now that we can make and receive API requests, let's add JSON support to the server middleware. @nuxt/http already has JSON support for making requests and parsing responses, so we need to add support on the API now. For this I use body-parser.

import { json } from 'body-parser'

export default [
  ...
  json(),
  (req, req, next) => {
    res.json = (obj) => res.write(JSON.stringify(obj))
    next()
  }
]

That json() call will return a working middleware function that peeks into requests, detects and parses JSON payloads, automatically making them available as req.body. I also added a little res.json() helper to quickly dump JSON responses.

Of course, we're still looking at the api.js file. The first middleware function is the /api/ping handler I've added earlier, hidden above for brevity. Note how in the second handler, next() is called at the end.

This is key to understanding server middleware handlers. If you don't call next(), no subsequent middleware will be executed and the response is finished and sent to the user. Calling next() is what lets us run multiple middleware serially until we get to Nuxt SSR phase.

Password hashing

Moving on with the boilerplate, let's redirect unauthenticated users to /login and also add a /register page. To register users, we'll use a mocked database. In this commit you'll see a db.js file as follows:

import bcrypt from 'bcrypt'

function hashPassword(password) {
  const salt = bcrypt.genSaltSync()
  return new Promise((resolve) => {
    bcrypt.hash(password, salt, (err, hash) => {
      resolve(err ? null : hash)
    })
  })
}

const db = {
  users: {}
}

export async function addUser(user) {
  user.password = await hashPassword(user.password)
  db.users[user.email] = user
}

That file is imported by api.js and used to handle POST /api/users:

{
  path: '/api/users',
  async handler(req, res, next) {
    if (req.method === 'POST') {
      await addUser(req.body)
      res.json({ success: true })
      res.end()
    }
    res.writeHead(403, 'Forbidden')
    res.end()
  }
}

Also in the same commit, you'll see I added the user state in store/index.js, proper login and register pages, with the register page already making an HTTP call to add a new user and redirect back to /login. You'll also notice an addition to the auth middleware:

export default function ({ store, route, redirect }) {
  if (!store.state.user.authenticated) {
    redirect('/register')
  }
}

JWT login

So far we can only get users registered (in our extremely volatile mock database), but none of them can still actually login. First there must be an /api/login endpoint which returns a JWT token, and code on the client-side to store it in a cookie so it will be sent automatically by the browser for every subsequent request. This commit adds the server code to generate JWT tokens and client code to store them. You'll see in that commit some markup fixes as well, among minor other changes. For authenticating users, db.js now exports authUser():

function checkPassword(password, user) {
  return new Promise((resolve) => {
    bcrypt.compare(password, user.password, (err, result) => {
      resolve(err ? false : result)
    })
  })
}

export function authUser(email, password) {
  if (email in db.users && db.users[email]) {
    return checkPassword(password, db.users[email])
  }
  return false
}

That is imported by api.js and used by the POST /api/login handler:

{
  path: '/api/login',
  async handler(req, res, next) {
    if (req.method === 'POST' && await authUser(req.body)) {
      const payload = { email: req.body.email }
      const token = sign(payload, sessionSecret, { expiresIn })
      res.json({ token })
      res.end()
      return
    }
    res.writeHead(403, 'Forbidden')
    res.end()
  }
}

Here's what our submit handler in /login looks like:

<template>
  <div>
    <h2>Login</h2>
    <input
      placeholder="Email"
      v-model="form.email">
    <input
      placeholder="Password"
      v-model="form.password">
    <button @click="login">
      Login
    </button>
  </div>
</template>

<script>
export default {
  data: () => ({
    form: {}
  }),
  methods: {
    async login() {
      const response = await
        this.$http.$post('api/login', this.form)
      if (response.token) {
        this.$store.commit('authUser', {
          email: this.form.email,
          token: response.token
        })
      }
      this.$router.push('/')
    }
  }
}
</script>

Retrieving cookies

So far, quickjam is able to register and login new users to its mock, in-memory database, but as soon you close the browser tab, you're logged out. There's no session persistence. We must store a cookie after login so that we can retrieve it in subsequent server requests.

I used the cookie (server) and js-cookie (client) libraries for this. There's probably a way to use the same library both on the client and server, but since docs on these are somewhat confusing, I'll stick to using both for now. Do let me know if you have a better approach.

In this commit, you'll see the addition of a new server middleware:

(req, res, next) => {
  const cookies = req.headers.cookie || ''
  const parsedCookies = parse(cookies) || {}
  const token = parsedCookies['quickjam-auth-token']
  if (token) {
    const jwtData = verify(token, sessionSecret)
    if (jwtData) {
      req.email = jwtData.email
      req.token = token
      return next()
    }
  }
  next()
}

Now, for every request, we check if there's a quickjam-auth-token cookie, and if that is a valid JWT token.

If it is, we store both email and token in the req object so we can pick them up further into in the Nuxt stack. We don't actually need to store req.token for this part, as we're able to authenticate the user in Nuxt middleware based on the presence of req.email alone, but having the token stored in the state will be necessary when we proceed to the next step, which is securing API requests.

To authenticate based on req.email, you can add the following bit to the auth middleware. Note how process.server is used to make sure a specific piece of code is only ran on the server. A more elegant way to achieve the same result is by using the nuxtServerInit action.

if (process.server && req.email) {
  store.commit('authUser', {
    token: req.token,
    email: req.email
  })
}

API authentication

At this point, we're able to register and login users, and also identify logged in users via a cookie. But we can't use a cookie to secure API requests because at some point, you're gonna have to run server-side API requests during login, and cookies aren't relayed from the browser to API during a server-side request, unless you manually do so. It is far more effective to employ an alternate authentication strategy, specific to API requests, that relies on the Authorization HTTP header rather than cookies.

To add an Authorization header to API requests, we can use @nuxt/http hooks. The next and final commit adds an http plugin to hook into API requests and make sure they go out with the JWT token:

export default function ({ $http, store }) {
  $http.onRequest((config) => {
    if (store.state.user.authenticated) {
      const auth = `Bearer ${store.state.user.token}`
      config.headers.set('Authorization', auth)
    }
    return config
  })
}

@nuxt/http offers slightly different API than @nuxtjs/axios for this, instead of simply assigning new keys to config.headers, you just use the set() method instead.

To check for the Authorization header server-side on API requests, I've added yet another server middleware which looks like this:

(req, res, next) => {
  if (!req.url.startsWith('/api')) {
    return next()
  }
  if (!req.headers.authorization) {
    res.statusCode = 401
    res.end()
    return
  }
  const tokenMatch = req.headers.authorization.match(/Bearer (.+)/)
  if (tokenMatch) {
    const jwtData = verify(tokenMatch[1], sessionSecret)
    if (jwtData) {
      req.email = jwtData.email
      req.token = tokenMatch[1]
      return next()
    }
  }
  res.statusCode = 401
  res.end()
}

The same commit also adds the GET /api/user API method, which will return all user data (including name). In index.vue, we now see:

export default {
  middleware: 'auth',
  data: () => ({
    user: {}
  }),
  async asyncData({ $http, store }) {
    return $http.$get('api/user')
  }
}

Conclusion

Make no mistake, you might very well find some holes in this setup. It is, like I mentioned earlier, meant to be a didactic example of how to effectively use and share data between server and routing middleware. In that regard I believe it succeeds in getting you started with a clear picture of all moving parts. Also, don't forget this uses an in-memory mock database, so every time your server restarts, db.users is reset. You'll want to refactor db.js to include code that talks to a real database server.

Also keep in mind this is far from an ideal setup. For better scability, you'll want to have your API separate from your Nuxt app. But like I said, this setup will actually go a long way for most apps, and is an extremely convenient and productive way to get your MVP started.

Soon there'll be an entirely different stack for getting backend API services bundled in a Nuxt app, so keep an eye on that!

Other resources