Writing Route Middlewares and Server Middlewares

Remember when you created a middleware on the server side using Koa in Chapter 8, Adding a Server-Side Framework? Middlewares are both useful and powerful, as you will have noted in cascading with Koa apps, where you can predict and control the flow of your entire app sequentially. So, what about in Nuxt? Well, there are two types of middleware we should explore in Nuxt: route middleware and server middleware. In this chapter, you will learn how to differentiate between them and create some basic middlewares before moving on to the next chapter on authentication, where middlewares are very much needed. We will also use middlewares in the chapters after the next one. So, in this chapter, just like in many of the previous chapters, you will create some middlewares in a Vue app with navigation guards so that you can grasp the middleware mechanism in the Vue/Nuxt system before creating the route middlewares and server middlewares in a Nuxt app. 

In this chapter, we will cover the following topics:

  • Writing middlewares with Vue Router
  • Introducing Vue CLI
  • Writing route middlewares in Nuxt
  • Writing Nuxt server middlewares

Writing middlewares with Vue Router

Before learning how middleware works in a Nuxt app, we should understand how it works in a standard Vue app. Additionally, before creating middlewares in the Vue app, let's first understand what they are.

What is middleware?

Put simply, a middleware is a software layer situated between two or more pieces of software. This is an old concept in software development. Middleware is a term that has been in use since 1968. It gained popularity in the 1980s as a solution to the problem of how to link newer apps to older legacy systems. There are many definitions for it, such as (from Google Dictionary) "[middleware is a] software that acts as a bridge between an operating system or database and apps, especially on a network."

In the web development world, server-side software or apps, such as Koa and Express, take in a request and output a response. The middlewares are programs or functions that are executed in the middle after the incoming request, and they produce an output that could either be the final output or be used by the next middleware until the cycle is completed. This also means that we can have more than one middleware, and they will execute in the order they are declared:

Furthermore, middleware is not only limited to server-side technologies. It is also very common on the client side when you have routing in your app. Vue Router by Vue.js is a good example of using this middleware concept. We have studied and used Vue Router already in Chapter 4, Adding Views, Routes, and Transitions, to create the router for our Vue apps. Now, let's dive deeper into the advanced usage of Vue Router – navigation guards.

Installing Vue Router

If you have followed the chapters of this book from the beginning, you should already know how to install Vue Router from Chapter 4, Adding Views, Routes, and Transitions. However, here is a quick recap.

Follow these steps to download Vue Router directly:

  1. Click on the following link and download the source code:
https://unpkg.com/vue-router/dist/vue-router.js
  1. Include the router after Vue so that it can be installed automatically by itself:
<script src="/path/to/vue.js"></script>
<script src="/path/to/vue-router.js"></script>

Alternatively, you can install Vue Router via npm:

  1. Install the router to your project using npm:
$ npm i vue-router
  1. Register the router explicitly using the use method:
import Vue from 'vue'
import VueRouter from 'vue-router'

Vue.use(VueRouter)
  1. Once you have the router in place, you can start creating middlewares using the navigation guards that come with Vue Router:
const router = new VueRouter({ ... })
router.beforeEach((to, from, next) => {
// ...
})

The beforeEach navigation guard in the preceding example is a global navigation guard that is called when navigating to any route. Apart from global guards, there are navigation guards for specific routes too, and that is what we are going to explore in more detail in the next section. So, let's get going!

If you want to find out more information about Vue Router, please visit https://router.vuejs.org/.

Using navigation guards

Navigation guards are used to guard the navigation in your app. These guards allow us to call functions before entering, updating, and leaving a route. When certain conditions are not met, they can either redirect or cancel the route. There are several ways in which to hook into the route navigation process: globally, per-route, or in-component. Let's explore the global guard in the next section.

Note that you can find all of the following examples in /chapter-11/vue/non-sfc/ from our GitHub repository.

Creating global guards

There are two global guards offered by Vue Router – global before guards and global after guards. Let's learn how to use them before applying them to our app:

  • Global before guards: Global before guards are called when a route is being entered. They are called in a specific order and can be asynchronous. The navigation is always in wait until all the guards are resolved. We can register these guards using the beforeEach method from Vue Router, as follows:
const router = new VueRouter({ ... })
router.beforeEach((to, from, next) => { ... })
  • Global after guards: Global after guards are called after a route has been entered. Unlike global before guards, global after guards do not have the next function, and so they do not affect navigation. We can register these guards using the afterEach method from Vue Router, as follows:
const router = new VueRouter({ ... })
router.afterEach((to, from) => { ... })

Let's create a Vue app with a simple HTML page and use these guards in the following steps:

  1. Create two routes with the <router-link> elements, as follows:
<div id="app">
<p>
<router-link to="/page1">Page 1</router-link>
<router-link to="/page2">Page 2</router-link>
</p>
<router-view></router-view>
</div>
  1. Define the components (Page1 and Page2) for the routes, and pass them to the router instance in the <script> block:
const Page1 = { template: '<div>Page 1</div>' }
const Page2 = { template: '<div>Page 2</div>' }

const routes = [
{ path: '/page1', component: Page1 },
{ path: '/page2', component: Page2 }
]

const router = new VueRouter({
routes
})
  1. Declare the global before guard and the global after guard after the route instance, as follows:
router.beforeEach((to, from, next) => {
console.log('global before hook')
next()
})

router.afterEach((to, from,) => {
console.log('global after hook')
})
  1. Mount the root instance after the guards and run our app:
const app = new Vue({
router
}).$mount('#app')
  1. Run the app in your browser, and you should get the following logs in the browser console when you switch between the routes:
global before hook
global after hook

Global guards can be useful when you want to apply something common to all routes. However, sometimes, we need something specific for certain routes only. For this, you should use per-route guards. Let's learn how to deploy them in the next section.

Creating per-route guards

We can create per-route guards by using beforeEnter as a method or property directly on the configuration object of a route. For example, take a look at the following:

beforeEnter: (to, from, next) => { ... }
// or:
beforeEnter (to, from, next) { ... }

Let's duplicate our previous Vue app and change the configuration of the routes to use these per-route guards, as follows:

const routes = [
{
path: '/page1',
component: Page1,
beforeEnter: (to, from, next) => {
console.log('before entering page 1')
next()
}
},
{
path: '/page2',
component: Page2,
beforeEnter (to, from, next) {
console.log('before entering page 2')
next()
}
}
]

You should get the before entering page 1 log on your browser's console when you navigate to /page1 and the before entering page 2 log when you are on /page2. So, since we can apply a guard to the route of a page, what about applying guards to the route component itself? The answer is yes, we can. Let's move on to the next section and learn how to use in-component guards to guard a specific component.

Creating in-component guards

We can use the following methods individually or together inside a route component to create the navigation guards for a specific component.

The beforeRouteEnter guard

Just like in the global before guard and the beforeEnter per-route guard, the beforeRouteEnter guard is called before the route renders the component, but it is applied to the component itself. We can register this type of guard using the beforeRouteEnter method, as follows:

beforeRouteEnter (to, from, next) { ... }

Because it is called before the component instance, it does not have access to the Vue component via the this keyword. But this can be resolved by passing a callback of the Vue component to the next argument:

beforeRouteEnter (to, from, next) {
next(vueComponent => { ... })
}

The beforeRouteLeave guard:

In comparison, the beforeRouteLeave guard is called when the component that is rendered by the route is about to navigate away from it. Since it is called when the Vue component is rendered, it can access the Vue component via the this keyword. We can register this type of guard using the beforeRouteLeave method, as follows:

beforeRouteLeave (to, from, next) { ... }

Usually, this type of guard is best used to prevent the user from leaving the route accidentally. So, the navigation can be canceled by calling next(false):

beforeRouteLeave (to, from, next) {
const confirmed = window.confirm('Are you sure you want to leave?')
if (confirmed) {
next()
} else {
next(false)
}
}

The beforeRouteUpdate guard:

The beforeRouteUpdate guard is called when the component that is rendered by the route has changed but the component is reused in the new route; for example, if you have subroute components that use the same route component: /page1/foo and /page1/bar. So, navigating from /page1/foo to /page1/bar will trigger this method. And since it is called when the component is rendered, it has access to the Vue component via the this keyword. We can register this type of guard using the beforeRouteUpdate method:

beforeRouteUpdate (to, from, next) { ... }

Note that the beforeRouteEnter method is the only guard that supports a callback in the next method. The Vue component is already available before calling the beforeRouteUpdate and beforeRouteLeave methods. Therefore, using a callback in the next method in either of them is unsupported because it is unnecessary. So, just use the this keyword if you want to access the Vue component:

beforeRouteUpdate (to, from, next) {
this.name = to.params.name
next()
}

Now, let's create a Vue app with a simple HTML page using the following guards:

  1. Create a page component with the beforeRouteEnter, beforeRouteUpdate, and beforeRouteLeave methods all together, as follows:
const Page1 = {
template: '<div>Page 1 {{ $route.params.slug }}</div>',
beforeRouteEnter (to, from, next) {
console.log('before entering page 1')
next(vueComponent => {
console.log('before entering page 1: ',
vueComponent.$route.path)
})
},
beforeRouteUpdate (to, from, next) {
console.log('before updating page 1: ', this.$route.path)
next()
},
beforeRouteLeave (to, from, next) {
console.log('before leaving page 1: ', this.$route.path)
next()
}
}
  1. Create another page component with just the beforeRouteEnter and beforeRouteLeave methods, as follows:
const Page2 = {
template: '<div>Page 2</div>',
beforeRouteEnter (to, from, next) {
console.log('before entering page 2')
next(vueComponent => {
console.log('before entering page 2: ',
vueComponent.$route.path)
})
},
beforeRouteLeave (to, from, next) {
console.log('before leaving page 2: ', this.$route.path)
next()
}
}
  1. Define the main routes and the subroute before initiating the router instance, as follows:
const routes = [
{
path: '/page1',
component: Page1,
children: [
{
path: ':slug'
}
]
},
{
path: '/page2',
component: Page2
}
]
  1. Create the navigation links with the <router-link> Vue component, as follows:
<div id="app">
<ul>
<li><router-link to="/">Home</router-link></li>
<li><router-link to="/page1">Page 1</router-link></li>
<li><router-link to="/page1/foo">Page 1: foo</router-link></li>
<li><router-link to="/page1/bar">Page 1: bar</router-link></li>
<li><router-link to="/page2">Page 2</router-link></li>
</ul>
<router-view></router-view>
</div>
  1. Run the app in your browser, and you should get the following logs in the browser console when switching between the routes:
  • When navigating from / to /page1, you should see the following:
before entering page 1
before entering page 1: /page1
  • When navigating from /page1 to /page2, you should see the following:
before leaving page 1: /page1
before entering page 2
before entering page 2: /page2
  • When navigating from /page2 to /page1/foo, you should see the following:
before leaving page 2: /page2
before entering page 1
before entering page 1: /page1/foo
  • When navigating from /page1/foo to /page1/bar, you should see the following:
before updating page 1: /page1/foo
  • When navigating from /page1/bar to /, you should see the following:
before leaving page 1: /page1/bar

As you can see, the navigation guards in Vue are simply JavaScript functions that allow us to create middlewares with some default arguments. Now, let's take a closer look at the arguments (to, from, and next) that each guard method gets in the next section.

Understanding the navigation guard arguments: to, from, and next

You have already seen these arguments in the navigation guards used in the previous sections, but we haven't walked you through them yet. All guards, except the afterEach global guard, use these three arguments: to, from, and next.

The to argument:

This argument is the route object that you navigate to (hence, it is called the to argument). This object holds the parsed information of the URL and the route:

name

meta

path

hash

query

params

fullPath

matched


If you want to know more about each of these object properties, please visit https://router.vuejs.org/api/the-route-object.

The from argument:

This argument is the current route object that you navigate from. Again, this object holds the parsed information of the URL and the route:

name

meta

path

hash

query

params

fullPath

matched


The
next argument:

This argument is a function you must call to move on to the next guard (middleware) in the queue. If you want to abort the current navigation, you can pass a false Boolean to this function:

next(false)

If you want to redirect to a different location, you can use the following line:

next('/')
// or
next({ path: '/' })

If you want to abort the navigation with an instance of Error, you can use the following lines:

const error = new Error('An error occurred!')
next(error)

Then, you can catch the error from the root:

router.onError(err
=> { ... })

Now, let's create a Vue app with a simple HTML page and experiment with the next function in the following steps:

  1. Create the following page components with the beforeRouteEnter method, as follows:
const Page1 = {
template: '<div>Page 1</div>',
beforeRouteEnter (to, from, next) {
const error = new Error('An error occurred!')
error.statusCode = 500
console.log('before entering page 1')
next(error)
}
}

const Page2 = {
template: '<div>Page 2</div>',
beforeRouteEnter (to, from, next) {
console.log('before entering page 2')
next({ path: '/' })
}
}

In the preceding code, we pass the Error instance to the next function for Page1 while redirecting the route to the home page for Page2.

  1. Define the routes before initiating the router instance, as follows:
const routes = [
{
path: '/page1',
component: Page1
},
{
path: '/page2',
component: Page2
}
]
  1. Create an instance of the router and listen to the error using the onError method:
const router = new VueRouter({
routes
})

router.onError(err => {
console.error('Handling this error: ', err.message)
console.log(err.statusCode)
})
  1. Create the following navigation links with the <router-link> Vue component:
<div id="app">
<ul>
<li><router-link to="/">Home</router-link></li>
<li><router-link to="/page1">Page 1</router-link></li>
<li><router-link to="/page2">Page 2</router-link></li>
</ul>
<router-view></router-view>
</div>
  1. Run the app in your browser, and you should get the following logs in the browser console when switching between the routes:
  • When navigating from / to /page1, you should see the following:
before entering page 1
Handling this error: An error occurred!
500
  • When navigating from /page1 to /page2, you should see the following:
before entering page 2

You will also notice that you are directed to / when navigating from /page1 to /page2 because of this line of code: next({ path: '/' }).

So far, we have created middlewares in a single HTML page. However, in a real-life project, we should try creating them with the Vue Single-File Component (SFC) that you learned about in previous chapters. So, in the next section, you will learn to create middlewares in the Vue SFC with Vue CLI, as opposed to the custom webpack build process that you have learned so far. So, let's get to it.

Introducing Vue CLI

We have used webpack to create our custom Vue SFC apps in Chapter 5, Adding Vue Components. As a developer, it is useful to know how to peer into the mechanism of a complex thing, and we must also understand how to use common and standard patterns to work with others collaboratively. Therefore, these days, we are inclined to use frameworks. Vue CLI is the standard tooling for Vue app development. It does what our webpack custom tool does and more. If you don't want to create your own Vue SFC developing tool, Vue CLI is a great choice. It supports Babel, ESLint, TypeScript, PostCSS, PWA, unit testing, and end-to-end testing out of the box. To read more about Vue CLI, please visit https://cli.vuejs.org/.

Installing Vue CLI

It is very easy to get started with Vue CLI. Perform these steps:

  1. Use npm to install it globally:
$ npm i -g @vue/cli
  1. Create a project when you want to:
$ vue create my-project
  1. You will be prompted to pick a preset – default or manually select features, as follows:
Vue CLI v4.4.6
? Please pick a preset: (Use arrow keys)
> default (babel, eslint)
Manually select features
  1. Choose the default preset as we can install what we need manually later on. You should see something similar to the last part of the following output in your terminal when the installation is complete:
Successfully created project my-project. 
Get started with the following commands:

$ cd my-project
$ npm run serve

  1. Change your directory to my-project and start the development process:
$ npm run serve

You should get something similar to this:

 DONE Compiled successfully in 3469ms

App running at:
- Local: http://localhost:8080/
- Network: http://199.188.0.44:8080/

Note that the development build is not optimized.
To create a production build, run npm run build.

In the following sections, we are going to transform the navigation guards that you learned about in the previous sections into proper middlewares using Vue CLI. That means we will separate all of the hooks and guards into separate .js files and keep them in a common folder called middlewares. However, before we do that, we should first understand the project directory structure that Vue CLI generates for us and then add our own required directories. Let's get to it.

Understanding Vue CLI's project structure

After creating the project with Vue CLI, if you take a look inside the project directory, you'll see that it provides us with a barebones structure, as follows:

├── package.json
├── babel.config.js
├── README.md
├── public
│ ├── index.html
│ └── favicon.ico
└── src
├── App.vue
├── main.js
├── router.js
├── components
│ └── HelloWorld.vue
└── assets
└── logo.png

From this basic structure, we can build and grow our app. So, let's develop our app in the /src/ directory and add the following directories to it using a router file:

└── src
├── middlewares/
├── store/
├── routes/
└── router.js

We will create two route components, login and secured, as SFC pages, and make the secured page a 403 protected page, which will require the user to log in to provide their name and age to access the page. The following are the files and the structure inside the /src/ directory that we will need for this simple Vue app:

└── src
├── App.vue
├── main.js
├── router.js
├── components
│ ├── secured.vue
│ └── login.vue
├── assets
│ └── ...
├── middlewares
│ ├── isLoggedIn.js
│ └── isAdult.js
├── store
│ ├── index.js
│ ├── mutations.js
│ └── actions.js
└── routes
├── index.js
├── secured.js
└── login.js

We now have an idea of what directories and files we need for our app. Next, we will move on to writing the code for these files.

Writing middlewares and a Vuex store with Vue CLI

If you take a look at package.json, you will see that the default dependencies that come with Vue CLI are very basic and minimal:

// package.json
"dependencies": {
"core-js": "^2.6.5",
"vue": "^2.6.10"
}

So, we will install our project dependencies and write the code we need in the following steps:

  1. Install the following packages via npm:
$ npm i vuex
$ npm i vue-router
$ npm i vue-router-multiguard
Note that Vue does not support multiple guards per route. So, if you want to create more than one guard for a route, Vue Router Multiguard allows you to do this. For more information about this package, please visit https://github.com/atanas-dev/vue-router-multiguard.
  1. Create the state, actions, and mutations to store the authenticated user details in the Vuex store so that these details can be accessed by any component:
// src/store/index.js
import Vue from 'vue'
import Vuex from 'vuex'

import actions from './actions'
import mutations from './mutations'

Vue.use(Vuex)

export default new Vuex.Store({
state: { user: null },
actions,
mutations
})

For the sake of readability and simplicity, we will separate the store's actions into a separate file, as follows:

// src/store/actions.js
const actions = {
async login({ commit }, { name, age }) {
if (!name || !age) {
throw new Error('Bad credentials')
}
const data = {
name: name,
age: age
}
commit('setUser', data)
},

async logout({ commit }) {
commit('setUser', null)
}
}
export default actions

We will also separate the store's mutations into a separate file, as follows:

// src/store/mutations.js
const mutations = {
setUser (state, user) {
state.user = user
}
}
export default mutations
  1. Create a middleware to ensure the user has logged in:
// src/middlewares/isLoggedIn.js
import store from '../store'

export default (to, from, next) => {
if (!store.state.user) {
const err = new Error('You are not connected')
err.statusCode = 403
next(err)
} else {
next()
}
}
  1. Create another middleware to ensure the user is over 18 years old:
// src/middlewares/isAdult.js
import store from '../store'

export default (to, from, next) => {
if (store.state.user.age < 18) {
const err = new Error('You must be over 18')
err.statusCode = 403
next(err)
} else {
next()
}
}
  1. Import these two middlewares in the secured route by using vue-router-multiguard to insert multiple middlewares in beforeEnter:
// src/routes/secured.js
import multiguard from 'vue-router-multiguard'
import secured from '../components/secured.vue'
import isLoggedIn from '../middlewares/isLoggedIn'
import isAdult from '../middlewares/isAdult'

export default {
name: 'secured',
path: '/secured',
component: secured,
beforeEnter: multiguard([isLoggedIn, isAdult])
}
  1. Create a client-side authentication with a simple login page. Here are the basic input fields we need for the login and logout methods:
// src/components/login.vue
<form @submit.prevent="login">
<p>Name: <input v-model="name" type="text" name="name"></p>
<p>Age: <input v-model="age" type="number" name="age"></p>
<button type="submit">Submit</button>
</form>

export default {
data() {
return {
error: null,
name: '',
age: ''
}
},
methods: {
async login() { ... },
async logout() { ... }
}
}
  1. Complete the preceding login and logout methods by dispatching the login and logout action methods in the try and catch blocks, as follows:
async login() {
try {
await this.$store.dispatch('login', {
name: this.name,
age: this.age
})
this.name = ''
this.age = ''
this.error = null
} catch (e) {
this.error = e.message
}
},
async logout() {
try {
await this.$store.dispatch('logout')
} catch (e) {
this.error = e.message
}
}
  1. Import the completed login component into the login route, as follows:
// src/routes/login.js
import Login from '../components/login.vue'

export default {
name: 'login',
path: '/',
component: Login
}

Note that we name this route login because we will need this name later to redirect the navigation route when we get the authentication error from the preceding middlewares.

  1. Import the login and secured routes to the index route, as follows:
// src/routes/index.js
import login from './login'
import secured from './secured'

const routes = [
login,
secured
]

export default routes
  1. Import the preceding index route into the Vue Router instance and catch the route errors using router.onError, as follows:
// src/router.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import Routes from './routes'

Vue.use(VueRouter)

const router = new VueRouter({
routes: Routes
})

router.onError(err => {
alert(err.message)
router.push({ name: 'login' })
})

export default router

In this step, we use router.onError to handle the Error object that passed from the middlewares, and router.push to redirect the navigation route to the login page when the authentication conditions are not met. The name of the object must be the same as the login route in step 7, which is login.

  1. Import the router and store it in the main file:
// src/main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'

new Vue({
router,
store,
render: h => h(App),
}).$mount('#app')
  1. Run the project with npm run serve, and you should see that the app is loaded at localhost:8080. If you type a name and a number less than 18 into the input fields on the home page and then click on the login button, you should get an alert saying that "You must be over 18" when trying to access the secured page. On the other hand, if you type in a number higher than 18, you should see the name and the number on the secured page:
Name: John
Age: 20
You can find the entire code for this app in /chapter-11/vue/vue-cli/basic/ from our GitHub repository. You can also find the app with the custom webpack in /chapter-11/vue/webpack/.

Well done! You have managed to get through all of the sections on middleware for Vue projects. Now, let's apply what you have just learned about Nuxt projects in the upcoming section.

Writing route middlewares in Nuxt

As always, once we understand how middleware works in Vue, it will then be easier to work with it in Nuxt as Nuxt has taken care of Vue Router for us. In the coming sections, we will learn how to work with global and per-route middlewares for Nuxt apps.

In Nuxt, all middleware should be kept in the /middleware/ directory, and the middleware filename will be the name of the middleware. For example, /middleware/user.js is the user middleware. A middleware gets the Nuxt context as its first argument:

export default (context) => { ... }

Additionally, middleware can be asynchronous:

export default async (context) => {
const { data } = await axios.get('/api/path')
}

In universal mode, middlewares are called on the server side once (for example, when first requesting the Nuxt app or when refreshing a page) and then on the client side when navigating to other routes. On the other hand, middlewares are always called on the client side whether you are requesting the app for the first time or when you are navigating to further routes after the first request. Middlewares are executed in the Nuxt configuration file first, then in layouts, and, finally, in pages. We will now start writing some global middlewares in the next section.

Writing global middlewares

Adding global middlewares is very straightforward; you just have to declare them in the middleware key in the router option of the config file. For example, take a look at the following:

// nuxt.config.js
export default {
router: {
middleware: 'auth'
}
}

Now, let's create some global middleware in the following steps. In this exercise, we want to to get the information of the user agent from the HTTP request header and to track the routes the user is navigating to:

  1. Create two middlewares in the /middleware/ directory, one for obtaining the user agent information and the other for obtaining the route path information that the user is navigating to:
// middleware/user-agent.js
export default (context) => {
context.userAgent = process.server ? context.req.headers[
'user-agent'] : navigator.userAgent
}

// middleware/visits.js
export default ({ store, route, redirect }) => {
store.commit('addVisit', route.path)
}
  1. Declare the preceding middlewares in the middleware key in the router option, as follows:
// nuxt.config.js
module.exports = {
router: {
middleware: ['visits', 'user-agent']
}
}
Note that, in Nuxt, we do not need a third-party package like we do in the Vue app to call multiple guards.
  1. Create the store's state and mutations for storing the visited route:
// store/state.js
export default () => ({
visits: []
})

// store/mutations.js
export default {
addVisit (state, path) {
state.visits.push({
path,
date: new Date().toJSON()
})
}
}
  1. Use the user-agent middleware in the about page:
// pages/about.vue
<p>{{ userAgent }}</p>

export default {
asyncData ({ userAgent }) {
return {
userAgent
}
}
}
  1. As for visits middleware, we want to use it on a component and then inject this component into our layout, that is, the default.vue layout. First, create the visits component in the /components/ directory:
// components/visits.vue
<li v-for="(visit, index) in visits" :key="index">
<i>{{ visit.date | dates }} | {{ visit.date | times }}</i> - {{
visit.path }}
</li>

export default {
filters: {
dates(date) {
return date.split('T')[0]
},
times(date) {
return date.split('T')[1].split('.')[0]
}
},
computed: {
visits() {
return this.$store.state.visits.slice().reverse()
}
}
}

So, we have created two filters in this component. The date filter is used to obtain the date from a string. For example, we will get 2019-05-24 from 2019-05-24T21:55:44.673Z. In comparison, the time filter is used to obtain the time from a string. For example, we will get 21:55:44 from 2019-05-24T21:55:44.673Z.

  1. Import the visits component into our layout:
// layouts/default.vue
<template>
<Visits />
</template>

import Visits from '~/components/visits.vue'
export default {
components: {
Visits
}
}

We should get the following results in our browser when we navigate around the routes:

2019-06-06 | 01:55:44 - /contact
2019-06-06 | 01:55:37 - /about
2019-06-06 | 01:55:30 - /

Additionally, we should get the information of the user agent from the request headers when you are on the about page:

Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.75 Safari/537.36
You can find the preceding source code in /chapter-11/nuxt-universal/route-middleware/global/ in our GitHub repository.

That's all for global middleware. Now, let's move on to per-route middlewares in the next section.

Writing per-route middlewares

Adding per-route middlewares is also very straightforward; you just have to declare them in the middleware key in the specific layout or page. For example, take a look at the following:

// pages/index.vue or layouts/default.vue
export default {
middleware: 'auth'
}

So, let's create some per-route middlewares in the following steps. In this exercise, we will use sessions and JSON Web Tokens (JWTs) to access restricted pages or a secured API. Although in real life, we can either just use a session or a token for an authentication system, we will use both for our exercise so that we know how to use them together for potentially more complex production systems. In our exercise, we will want the user to log in and get the token from the server. The user will be not able to access the secured routes when the token is expired or invalid.

Additionally, the user will be logged out when the session time is over:

  1. Create an auth middleware to check whether the state in our store has any data. If there is no authenticated data, then we use the error function in the Nuxt context to send the error to the front:
// middleware/auth.js
export default function ({ store, error }) {
if (!store.state.auth) {
error({
message: 'You are not connected',
statusCode: 403
})
}
}
  1. Create a token middleware to ensure the token is in the store; otherwise, it sends the error to the front. If the token is present in the store, we set Authorization with the token to the default axios header:
// middleware/token.js
export default async ({ store, error }) => {
if (!store.state.auth.token) {
error({
message: 'No token',
statusCode: 403
})
}
axios.defaults.headers.common['Authorization'] = `Bearer: ${store.state.auth.token}`
}
  1. Add these two preceding middlewares to the middleware key on the secured page:
// pages/secured.vue
<p>{{ greeting }}</p>

export default {
async asyncData ({ redirect }) {
try {
const { data } = await axios.get('/api/private')
return {
greeting: data.data.message
}
} catch (error) {
if(process.browser){
alert(error.response.data.message)
}
return redirect('/login')
}
},
middleware: ['auth', 'token']
}

After setting the Authorization header with the JWT in the headers, we can access the secured API routes, which are guarded by a server-side middleware (we will learn more about this in Chapter 12, Creating User Logins and API Authentication). We will get the data from the secured API route that we want to access and will be prompted with the error message if the token is incorrect or has expired.

  1. Create the store's state, mutations, and actions in the /store/ directory to store the authenticated data:
// store/state.js
export default () => ({
auth: null
})

// store/mutations.js
export default {
setAuth (state, data) {
state.auth = data
}
}

// store/actions.js
export default {
async login({ commit }, { username, password }) {
try {
const { data } = await axios.post('/api/public/users/login',
{ username, password })
commit('setAuth', data.data)
} catch (error) {
// handle error
}
},

async logout({ commit }) {
await axios.post('/api/public/users/logout')
commit('setAuth', null)
}
}

A known and expected behavior is that, when the​ page is refreshed, the state of the store is reset to default. There are a few solutions that we can use if we want to persist in the state:

  1. localStorage
  2. sessionStorage
  3. vuex-persistedstate (a Vuex plugin)

In our case, however, since we use the session to store the authenticated information, we can actually retrace our data from the session via the following:

  1. req.ctx.session (Koa) or req.session (Express)
  2. req.headers.cookie

Once we have decided which solution or option we want to go for (let's say req.headers.cookie), then we can refill the state as follows:

// store/index.js
const cookie = process.server ? require('cookie') : undefined

export const actions = {
nuxtServerInit({ commit }, { req }) {
var session = null
var auth = null
if (req.headers.cookie && req.headers.cookie.indexOf('koa:sess') > -1) {
session = cookie.parse(req.headers.cookie)['koa:sess']
}
if (session) {
auth = JSON.parse(Buffer.from(session, 'base64'))
commit('setAuth', auth)
}
}
}
You can find the preceding source code in /chapter-11/nuxt-universal/route-middleware/per-route/ in our GitHub repository.

When all the preceding steps are followed and the middlewares are created, we can run this simple authentication app with npm run dev to see how it works. We will get to server-side authentication in the next chapter. Right now, we just have to focus on the middleware and understand how it works, which will help us in the next chapter. Now, let's move on to the final part of this chapter – server middlewares.

Writing Nuxt server middlewares

Put simply, server middlewares are server-side apps that are used as middlewares in Nuxt. We have been running our Nuxt apps under a server-side framework such as Koa since Chapter 8, Adding a Server-Side Framework. If you are using Express, this is the scripts object in your package.json file:

// package.json
"scripts": {
"dev": "cross-env NODE_ENV=development nodemon server/index.js --watch
server",
"build": "nuxt build",
"start": "cross-env NODE_ENV=production node server/index.js",
"generate": "nuxt generate"
}

In this npm script, the dev and start scripts instruct the server to run your app from /server/index.js. This might not be ideal as we have coupled Nuxt and the server-side framework tightly, and it results in extra work in the configuration. However, we can tell Nuxt not to attach to the server-side framework's configurations in /server/index.js and keep our original Nuxt run scripts as they are shown here:

// package.json
"scripts": {
"dev": "nuxt",
"build": "nuxt build",
"start": "nuxt start",
"generate": "nuxt generate"
}

On the contrary, we can have the server-side framework running under Nuxt instead, by using the serverMiddleware property in the Nuxt configuration file. For example, take a look at the following:

// nuxt.config.js
export default {
serverMiddleware: [
'~/api'
]
}

Unlike route middlewares, which are called before each route on the client side, server middlewares are always called on the server side before vue-server-renderer. Therefore, server middlewares can be used for server-specific tasks, just like we did with Koa or Express in previous chapters. So, let's explore how to use Express and Koa as our server middleware in the next sections.

Using Express as Nuxt's server middleware

Let's create a simple authentication app using Express as Nuxt's server middleware. We will be still using the client-side code from the authentication exercise along with the per-route middlewares that you learned about in the preceding section, where the user is required to provide a username and password to access a secured page. Additionally, we will be using a Vuex store to centralize the authenticated user data just like before. The major difference in this exercise is that our Nuxt app will be moved out of the server-side app as a middleware, and, instead, the server-side app will be moved into the Nuxt app as middleware. So, let's get started by following these steps:

  1. Install cookie-session and body-parser as server middlewares, and add the path of our API after them in the Nuxt config file, as follows:
// nuxt.config.js
import bodyParser from 'body-parser'
import cookieSession from 'cookie-session'

export default {
serverMiddleware: [
bodyParser.json(),
cookieSession({
name: 'express:sess',
secret: 'super-secret-key',
maxAge: 60000
}),
'~/api'
]
}

Note that cookie-session is a cookie-based session middleware for Express that stores the session in a cookie on the client side. In comparison, body-parser is a body-parsing middleware for Express, just like the koa-bodyparser for Koa that you learned about in Chapter 8Adding a Server-Side Framework.

For more information about cookie-session and body-parser for Express, please visit https://github.com/expressjs/cookie-session and https://github.com/expressjs/body-parser.
  1. Create an /api/ directory using an index.js file, in which Express is imported and exported as another server middleware:
// api/index.js
import express from 'express'
const app = express()

app.get('/', (req, res) => res.send('Hello World!'))

// Export the server middleware
export default {
path: '/api',
handler: app
}
  1. Run the app using npm run dev, and you should get the "Hello World!" message in localhost:3000/api.
  2. Add the login and logout post methods in /api/index.js, as follows:
// api/index.js
app.post('/login', (req, res) => {
if (req.body.username === 'demo' && req.body.password === 'demo') {
req.session.auth = { username: 'demo' }
return res.json({ username: 'demo' })
}
res.status(401).json({ message: 'Bad credentials' })
})

app.post('/logout', (req, res) => {
delete req.session.auth
res.json({ ok: true })
})

In the preceding code, we store the authenticated payload to the Express session as auth in the HTTP request object when the user has logged in successfully. Then, we will clear the auth session by deleting it when the user has logged out.

  1. Create a store with state.js and mutations.js, just like you did for writing a per-route middleware, as follows:
// store/state.js
export default () => ({
auth: null,
})

// store/mutations.js
export default {
setAuth (state, data) {
state.auth = data
}
}
  1. Just as with writing per-route middleware, create the login and logout action methods in the actions.js file in the store, as follows:
// store/actions.js
import axios from 'axios'

export default {
async login({ commit }, { username, password }) {
try {
const { data } = await axios.post('/api/login', { username,
password })
commit('setAuth', data)
} catch (error) {
// handle error...
}
},

async logout({ commit }) {
await axios.post('/api/logout')
commit('setAuth', null)
}
}
  1. Add a nuxtServerInit action to index.js in the store to repopulate the state from the Express session in the HTTP request object when refreshing pages:
// store/index.js
export const actions = {
nuxtServerInit({ commit }, { req }) {
if (req.session && req.session.auth) {
commit('setAuth', req.session.auth)
}
}
}
  1. Finally, just like in per-route middleware authentication, create a login page in the /pages/ directory with a form. Use the same login and logout methods you did before to dispatch the login and logout action methods in the store:
// pages/index.vue
<form v-if="!$store.state.auth" @submit.prevent="login">
<p v-if="error" class="error">{{ error }}</p>
<p>Username: <input v-model="username" type="text"
name="username"></p>
<p>Password: <input v-model="password" type="password"
name="password"></p>
<button type="submit">Login</button>
</form>

export default {
data () {
return {
error: null,
username: '',
password: ''
}
},
methods: {
async login () { ... },
async logout () { ... }
}
}
  1. Run the app with npm run dev. You should have an authentication app that works just like before, but it is no longer running from /server/index.js.
You can find the preceding source code in /chapter-11/nuxt-universal/server-middleware/express/ in our GitHub repository.

Using the serverMiddleware property allows our Nuxt app to look neat and feel light again by freeing it from the server-side app, don't you think? With this approach, we can make it more flexible too, as we can use any server-side framework or app. For example, instead of using Express, we can use Koa, which we will look at in the next section.

Using Koa as Nuxt's server middleware

Just like Koa and Express, Connect is a simple framework that is used to glue together various middlewares for handling HTTP requests. Nuxt internally uses Connect as a server, so most Express middleware works with Nuxt's server middleware. In comparison, it is a bit harder for Koa middleware to work as Nuxt's server middleware because the req and res objects are tucked away and kept inside ctx in Koa. We can compare these three frameworks with a simple "Hello World" message, as follows:

// Connect
const connect = require('connect')
const app = connect()
app.use((req, res, next) => res.end('Hello World'))

// Express
const express = require('express')
const app = express()
app.get('/', (req, res, next) => res.send('Hello World'))

// Koa
const Koa = require('koa')
const app = new Koa()
app.use(async (ctx, next) => ctx.body = 'Hello World')

Notice that req is a Node.js HTTP request object, while res is a Node.js HTTP response object. They can be named anything you like, for example, request instead of req and response instead of res. From the preceding comparisons, you can see how Koa handles these two objects differently from the other frameworks. So, we can't use Koa as Nuxt's server middleware like in Express, and we can't define any Koa middleware in the serverMiddleware property but just add the path of the directory where the Koa API is kept. Rest assured, it is not difficult to get them working as middleware in our Nuxt app. Let's proceed with the following steps:

  1. Add the path where we want to create our API with Koa, as follows:
// nuxt.config.js
export default {
serverMiddleware: [
'~/api'
]
}
  1. Import koa and koa-router, create a Hello World! message with the router, and then export them to the index.js file inside the /api/ directory:
// api/index.js
import Koa from 'koa'
import Router from 'koa-router'

router.get('/', async (ctx, next) => {
ctx.type = 'json'
ctx.body = {
message: 'Hello World!'
}
})

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

// Export the server middleware
export default {
path: '/api',
handler: app.listen()
}
  1. Import koa-bodyparser and koa-session, and register them as middlewares in the Koa instance in the /api/index.js file, as follows:
// api/index.js
import bodyParser from 'koa-bodyparser'
import session from 'koa-session'

const CONFIG = {
key: 'koa:sess',
maxAge: 60000,
}

app.use(session(CONFIG, app))
app.use(bodyParser())
  1. Create the login and logout routes using the Koa router, as follows:
// api/index.js
router.post('/login', async (ctx, next) => {
let request = ctx.request.body || {}
if (request.username === 'demo' && request.password === 'demo') {
ctx.session.auth = { username: 'demo' }
ctx.body = {
username: 'demo'
}
} else {
ctx.throw(401, 'Bad credentials')
}
})

router.post('/logout', async (ctx, next) => {
ctx.session = null
ctx.body = { ok: true }
})

In the preceding code, just like in the Express example in the previous section, we store the authenticated payload to the Koa session as auth in the Koa context object when the user has logged in successfully. Then, we will clear the auth session by setting the session to null when the user has logged out.

  1. Create a store with the state, mutations, and actions just like you did in the Express example. Additionally, create nuxtServerInit in the index.js file in the store just like you did when writing per-route middlewares:
// store/index.js
export const actions = {
nuxtServerInit({ commit }, { req }) {
// ...
}
}
  1. Just like before, create the form login and logout methods in the /pages/ directory to dispatch the action methods from the store:
// pages/index.vue
<form v-if="!$store.state.auth" @submit.prevent="login">
//...
</form>

export default {
methods: {
async login () { ... },
async logout () { ... }
}
}
  1. Run the app with npm run dev. You should have an authentication app that works just like the one in Express in the previous section, but it is no longer running from /server/index.js.
You can find the entire source code for this example in /chapter-11/nuxt-universal/server-middleware/koa/ in our GitHub repository.

Depending on your preference, you can use Express or Koa as Nuxt's server middleware in your next project. In this book, we mostly use Koa for its simplicity. You even can create custom server middleware without needing either of them. Let's take a look at how to create custom server middleware in the next section.

Creating custom server middleware

Since Nuxt internally uses Connect as the server, we can add our custom middlewares without the need for an external server such as Koa or Express. You can develop a complex Nuxt server middleware just like we did with Koa and Express in the previous sections. However, let's not endlessly repeat what we have already done. Let's create a very basic custom middleware that prints a "Hello World" message to confirm the feasibility of building a complex one from a basic middleware in the following steps:

  1. Add the path where we want to create our custom middleware:
// nuxt.config.js
serverMiddleware: [
{ path: '/api', handler: '~/api/index.js' }
]
  1. Add the API routes to the index.js file inside the /api/ directory:
// api/index.js
export default function (req, res, next) {
res.end('Hello world!')
}
  1. Run the app with npm run dev and navigate to localhost:3000/api. You should see the "Hello World!" message printed on your screen.
You can refer to the Connect documentation at https://github.com/senchalabs/connect for more information. Additionally, you can find the source code for this example in /chapter-11/nuxt-universal/server-middleware/custom/ in our GitHub repository.

Well done! You have managed to get through another big chapter on Nuxt. Before moving on to the next chapter, let's summarize what you have learned so far.

Summary

In this chapter, you learned the distinction between route middleware and server middleware. You used navigation guards from Vue Router to create middlewares for Vue apps. You also used Vue CLI to develop a simple Vue authentication app. Taking what you learned about Vue apps, you implemented the same concept (of route middleware) in Nuxt apps with global and per-route middlewares. After that, you managed to learn about Nuxt's server middleware and how to use Express and Koa as server middlewares. Middlewares are important and useful, especially for authentication and security. We have already made a few authentication apps, and we are going to study and understand them in more detail in the next chapter.

In the next chapter, you will learn, in detail, about developing user logins and authentication APIs to improve the authentication apps that you have created in this chapter. We will walk you through session-based authentication and token-based authentication. While you have created an authentication app using these two technologies, we haven't explained what they are yet. But rest assured, you will understand them better in the next chapter. Besides this, you will learn how to create backend and frontend authentication and signing in with Google OAuth for your Nuxt app. So, stay tuned!

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.14.132.214