updated on

Writing koa/nuxt applications

Introduction

You’re ready to make your new application.
You take Vue 2 as your framework

But you want your application to be:

  • fast
  • bulletproof

So you decide to make a Universal Web Application with Nuxt 2 & Koa 2
It will:

  • fasten the first rendering
  • be able to run without JS activated on the client side

Notes:

  • You will need to be familiar with Koa/Nuxt.
  • Be aware that both Nuxt and Koa use the concepts of context (ctx) & middleware.
    I’ve tried to differentiate them as much as I could in the following post, but if you’re confused reread carefully and try to sort it out 😅
  • You can find a working example of what I’m talking next in the koa-nuxt-example repository

Shaped for express

Because express.js is the most commonly used Node.js server framework, most of the UWA frameworks render function are shaped for it.

This means that in express, Nuxt integration came out of the box.
You just need to call it like any express middleware:

01-express-middleware.jsview raw
1
app.use(nuxt.render)

Since we want to use Koa, we will need to make our own middleware.
No need to think a lot about integration since this has already been solved but the Nuxt community

02-koa-middleware.jsview raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
async function nuxtMiddleware(ctx) {
// koa defaults to 404 when it sees that status is unset
ctx.status = 200

return new Promise((resolve, reject) => {
ctx.res.on('close', resolve)
ctx.res.on('finish', resolve)
nuxt.render(ctx.req, ctx.res, promise => {
// nuxt.render passes a rejected promise into callback on error.
promise.then(resolve).catch(reject)
})
})
}

app.use(nuxtMiddleware)

This will work perfectly if you’re only interested in server rendering.

Handling POST

Let say we want to be able to post a form.

  • First we will install/use koa-router and koa-body
  • Then with those, we will be able to handle our POST action
  • And we might want to do a database call inside it (doSomethingAsync in the example)
03-koa-form.jsview raw
1
2
3
4
5
6
7
8
9
10
11
12
13
// …koa app creation + middleware

const router = new Router()

router.post(`/my-action`, koaBody(), async ctx => {
const result = await doSomethingAsync(ctx.request.body)
ctx.redirect(`/`)
})

app.use(router.routes())
app.use(router.allowedMethods())

// …call our Nuxt middleware to handle anything that aren't our custom actions

This is kind of ok:

  • we can now post some data
  • redirect to / where Nuxt will handle the markup

JSON response can be added by later by

  • checking what’s the request Content-Type header (ctx.is('application/json'))
  • don’t redirect
  • send back the appropriate response

Handling errors

We should write an error middleware.
It will make sure that if something went wrong, our application won’t crash.
To catch all the things, it will be our first middleware.

04-koa-form-error.jsview raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// koa app creation + middleware + router creation

koa.use(async function handleErrors(ctx, next) {
try {
await next()
} catch (error) {
ctx.status = 500
ctx.body = error
}
})

router.post(`/my-action`, koaBody(), async ctx => {
const result = await doSomethingAsync(ctx.request.body)
ctx.redirect(`/`)
})

// mount router + Nuxt

So now if anything throw (DB call, JSON parsing…) we will render a page with the error printed.

Handling Server data with Nuxt

We also should send back some data validation to the user form.

a form field with a validation error
Oh no!

In order to display any validation in the Nuxt application we will need to:

  • persist data between our post route and the redirection
  • pass those data down to the Nuxt application
  • do something with it

Koa-session

The most common way to handle data between routes is with sessions.
We’ll use koa-session for this.

The installation guide is pretty self explanatory.
This will add a ctx.session object where we can pass any kind of information.

Here is the different steps to follow:

  • Validate our form
  • Add the validation to the session
  • Pass it to Nuxt
    • Because Nuxt doesn’t use the Koa ctx but use req & res, copy our session information into those objects.
    • This will be done in a Koa middleware just before the nuxt-rendering middleware
  • Integrate it in the Nuxt application by either using:
  • …and since now all is in the Vue realm, just use our Vue Components.
05-validation.jsview raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// after setting up koa-session

router.post(`/my-action`, koaBody(), async ctx => {
const { body } = ctx.request
// assuming that we have defined `isFormValid` before
const isValid = isFormValid(body)
if (isValid) {
const result = await doSomethingAsync(body)
} else {
// assuming that we have defined `getValidation` before
ctx.session.validation = getValidation(body)
}
ctx.redirect(`/`)
})

app.use(router.routes())
app.use(router.allowedMethods())

// put some data to the req object
// so we will be able to access them in the Nuxt app
// – in Vuex nuxtServerInit
// - in a Nuxt Middleware
app.use(async function passSessionToNuxt(ctx, next) {
ctx.req.serverData = {
validation: ctx.session.validation || {},
}
await next()
})

In the store/index.js

06-vuex-store.jsview raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
export const state = () => ({
validation: {},
})

export const UPDATE_VALIDATION = `UPDATE_VALIDATION`

export const mutations = {
[UPDATE_VALIDATION](state, payload) {
state.validation = payload
},
}

export const actions = {
nuxtServerInit({ commit }, nuxtCtx) {
const { req } = nuxtCtx
// here we find again our server data
const { serverData } = req

if (!serverData) return
if (serverData.validation) {
commit(UPDATE_VALIDATION, serverData.validation)
}
},
}

And that’s it, we now have a Vuex store updated with our server validation.
Use the mapState helper in our Vue component to access it.

Can’t set headers after they are sent

Right now, we set the validation on our POST route, and never update it again.

It means that the validation will be persisted until the user send a good form.
So if the user change page and go back to the form, the application will still display the last validation result.
This isn’t right, we should clear the validation once displayed.

This should be easy by updating our Koa middleware that link our session to nuxt.

07-naive-validation-clearing.jsview raw
1
2
3
4
5
6
7
8
app.use(async function passSessionToNuxt(ctx, next) {
ctx.req.serverData = {
validation: ctx.session.validation || {},
}
// don't persist the validation on more than one page
delete ctx.session.validation
await next()
})

⚠️ But this won’t work

You’ll find in the server logs a Can't set headers after they are sent.
The problem comes from the nuxtMiddleware & how it bypasses the regular Koa flow.

Usually we set a ctx.body and all the previous middleware will continue their work.

regular koa flow
regular flow

But that’s what happen here

koa-nuxt flow
koa-nuxt flow

To fix that we need to make sure that our headers are set before the Nuxt middleware.

autoCommit: false to the rescue

Koa-session lets us send the headers manually with the manuallyCommit() method

So we have to refactor our server code like this:

08-manually-commit.jsview raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
const sessionOptions = {
// don't autoCommit
// we will handle the header update ourself
autoCommit: false,
}
app.use(session(sessionOptions, app))

router.post(`/my-action`, koaBody(), async ctx => {
const { body } = ctx.request
const isValid = isFormValid(body)
if (isValid) {
const result = await doSomethingAsync(body)
} else {
ctx.session.validation = getValidation(body)
// set the headers manually
await ctx.session.manuallyCommit()
}
ctx.redirect(`/`)
})

// …mount the router…

app.use(async function passSessionToNuxt(ctx, next) {
ctx.req.serverData = {
validation: ctx.session.validation || {},
}
// don't persist the validation on more than one page
delete ctx.session.validation
// set the headers manually
await ctx.session.manuallyCommit()
await next()
})

// headers are set! we can safely call our Nuxt middleware

This will solve our problem ❤️
We just have now to remember calling manuallyCommit() every time we update the session… 😶

Displaying all errors with Nuxt

There is one last thing we have to take care of.
Right now our handleError middleware will make Koa show the error.
But Nuxt support an error layout and we should take advantage of it.

To do this we’ll need to modify our handleError middleware:

  • set the error to the ctx.req object (Remember Nuxt still only work with req & res)
  • call Nuxt to render the page inside our handleError middleware
  • write a Nuxt middleware that will render the error page by calling nuxtContext.error
09-koa-nuxt-error.jsview raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
koa.use(async function handleErrors(ctx, next) {
try {
await next()
} catch (error) {
ctx.status = 500
ctx.req.serverError = error
try {
// let Nuxt handle the response
await nuxtMiddleware(ctx)
} catch (nuxtError) {
// we tried our best
// but if something's still wrong, go without Nuxt
ctx.body = error
}
}
})

And for the Nuxt part:

  • create a middleware/handle-server-errors.js file
  • reference it in the nuxt.config.js
10-nuxt-error-middleware.jsview raw
1
2
3
4
5
6
7
8
9
10
11
export default function(nuxtContext) {
const { req, error } = nuxtContext
if (process.client) return
// here we catch the error send by the server
if (!req.serverError) return
// calling the error function will render the error layout
error({
statusCode: req.error.statusCode || 500,
message: req.error.message || `a fatal error as occurred`,
})
}

Conclusion

Making Nuxt working with Koa isn’t as smooth as with Express.
Still I prefer working with Koa, and with a little more boilerplate everything’s fine.

I’m sure there is room for improvements, but it’s working for me.
The downside is mainly more boilerplate code and handling session updates manually.

Most of the code here isn’t necessary if

  • you just want some basic server rendering
  • you don’t need to support any kind of session

Supporting asynchronous code should be easy

As a reminder you can find a full example here

💚 Nuxt 💙 KOA