Dotun Jolaoso Software Developer

Understanding Vue middleware pipelines

7 min read 2160

Generally, when building SPAs there’s a need to protect certain routes. For example, let’s say we have a dashboard route that can only be accessed by authenticated users, we can make use of an auth middleware to ensure that only authenticated users have access to the dashboard route.

In this tutorial, we’ll see how we can implement a middleware pipeline for a Vue app using Vue-Router.

What is a middleware pipeline?

A middleware pipeline is a stack of different middlewares ran in parallel with each other.

Using our hypothetical case study from the introductory paragraph, let’s say we have another route at /dashboard/movies, that we only want accessible to subscribed users. We already know that to access the dashboard route, you need to be authenticated. How then do we go about protecting the /dashboard/movies route to ensure that only authenticated and subscribed users have access to that route? By using a middleware pipeline, we can chain several middlewares together and ensure they are run in parallel.

Getting started

To get started, we’ll be using the Vue CLI to quickly scaffold a new Vue project.

vue create vue-middleware-pipeline

Installing dependencies

Once the project directory has been created and installed, navigate to the newly created directory and run the following command from the terminal:

npm i vue-router vuex

Vue-router —  is the official router for Vue.js

Vuex —  is a state management library for Vue

Creating components

Our application is going to have three components.

Login — This component will be displayed to users that have not been authenticated.

Dashboard — This component will be displayed to users that are signed in.

Movies — We’ll display this component to users that are signed in and have an active subscription.

Let’s create those components. Navigate to the src/components directory and create the following files: Dashboard.vue Login.vue and Movies.vue

Edit the Login.vue file with the following code:

<template>
  <div>
    <p>This is the Login component</p>
  </div>
</template>

Edit the Dashboard.vue file with the following code:

<template>
  <div>
    <p>This is the Dashboard component for authenticated users</p>
    <router-view/>
  </div>
</template>

Finally, add the following code to the Movies.vue file:

<template>
  <div>
    <p>This is the Movies component for authenticated and subscribed users</p>
  </div>
</template>

Creating the store

A store, in regards to Vuex , is simply a container for holding our application state. This will allow us to determine if a user is authenticated as well as check if the user is subscribed or not.
Inside the src folder, create a store.js file and add the following code to the file:

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)


export default new Vuex.Store({
    state: {
        user: {
            loggedIn: false,
            isSubscribed: false
        }
    },

    getters: {
        auth(state) {
            return state.user
        }
    }
})

The store contains a user object within its state. The user object contains a loggedIn and isSubscribed property which helps us determine if a user is logged in and has an active subscription. We’ve also defined a getter inside the store that returns the user object.

Defining our routes

Before we create our routes, let’s define them and associate the respective middlewares that will be attached to each of these routes.

/login will be accessible to everybody except authenticated users. When authenticated users visit this route, they should be redirected to the dashboard route. This route will have a guest middleware attached to it.

/dashboard will only be accessible to authenticated users. Unauthenticated users should be redirected to the /login route when they visit this route. We’ll associate an auth middleware with this route.

/dashboard/movies will be accessible to only authenticated and subscribed users. This route will be protected by a isSubscribed and auth middleware.

Creating the routes

Next, create a router folder in the src directory and then create a router.js file inside that folder. Edit the file with the following code:

import Vue from 'vue'
import Router from 'vue-router'
import store from '../store'

import Login from '../components/Login'
import Dashboard from '../components/Dashboard'
import Movies from '../components/Movies'


Vue.use(Router)

const router = new Router({
    mode: 'history',
    base: process.env.BASE_URL,
    routes: [
        {
            path: '/login',
            name: 'login',
            component: Login
        },

        {
            path: '/dashboard',
            name: 'dashboard',
            component: Dashboard,
            children: [{
                path: '/dashboard/movies',
                name: 'dashboard.movies',
                component: Movies
            }
        ],
        }
    ]
})


export default router

Here, we created a new router instance while passing in a couple of configuration options as well as a routes property which accepts all the routes we defined earlier. At the moment, it’s important to note that none of these routes are protected. We’ll fix that shortly.
Next, let’s inject the router and our store into the Vue instance. Edit the src/main.js file with the code below:

import Vue from 'vue'
import App from './App.vue'
import router from './router/router'
import store from './store'

Vue.config.productionTip = false


new Vue({
  router,
  store,
  render: h => h(App),
}).$mount('#app')

Creating the middlewares

In the src/router directory, create a middleware folder and then create guest.js ,auth.js and IsSubscribed.js file under that particular folder. Add the following code to the guest.js file:

export default function guest ({ next, store }){
    if(store.getters.auth.loggedIn){
        return next({
           name: 'dashboard'
        })
    }
   
    return next()
   }

The guest middleware checks to see if the user is authenticated. If they’re authenticated, they are redirected to the dashboard route.

Next, edit the auth.js file with the following code:

export default function auth ({ next, store }){
 if(!store.getters.auth.loggedIn){
     return next({
        name: 'login'
     })
 }

 return next()
}

In the auth middleware, using the store we check to see whether the user is currently authenticated. Depending on if the user is logged in or not, we either continue with the request or redirect them to the login page.

Edit the isSubscribed.js file with the following code:

export default function isSubscribed ({ next, store }){
    if(!store.getters.auth.isSubscribed){
        return next({
           name: 'dashboard'
        })
    }
   
    return next()
   }

The isSubscribed middleware is similar to the auth middleware. Using our store we check to see if the user is subscribed. If the user is subscribed, they can access the intended route or else they are redirected back to the dashboard page.

Protecting the routes

Now that we’ve created all our middlewares, let’s make use of them to protect our routes. Edit the src/router/router.js file with the following code:

import Vue from 'vue'
import Router from 'vue-router'
import store from '../store'

import Login from '../components/Login'
import Dashboard from '../components/Dashboard'
import Movies from '../components/Movies'

import guest from './middleware/guest'
import auth from './middleware/auth'
import isSubscribed from './middleware/isSubscribed'


Vue.use(Router)

const router = new Router({
    mode: 'history',
    base: process.env.BASE_URL,
    routes: [{
            path: '/login',
            name: 'login',
            component: Login,
            meta: {
                middleware: [
                    guest
                ]
            }
        },

        {
            path: '/dashboard',
            name: 'dashboard',
            component: Dashboard,
            meta: {
                middleware: [
                    auth
                ]
            },
            children: [{
                path: '/dashboard/movies',
                name: 'dashboard.movies',
                component: Movies,
                meta: {
                    middleware: [
                        auth,
                        isSubscribed
                    ]
                }
            }],
        }
    ]
})


export default router

Here, we imported all our middlewares and then for each of the routes we defined a meta field that contains a middleware array. The middleware array contains all the middlewares we want to be associated with a particular route.

Vue Router navigation guards

The navigation guards provided by Vue Router are the only reason why we can use a middleware to protect our routes in the first place. These navigation guards are primarily used to guard routes either by redirecting or canceling it.

One of those guards is the global before guard which is usually a hook called just before a route is triggered. To register a global before guard, we define a beforeEach method on the router instance.

const router = new Router({ ... })
router.beforeEach((to, from, next) => {
 //necessary logic to resolve the hook
})

The beforeEach method receives three arguments:

to: This is the route we intend on accessing.

from: This is the route we’re currently coming from.

next: This is the function called to resolve the hook.

Running the middleware

Using the beforeEach hook, we can run our middleware.

const router = new Router({ ...})

router.beforeEach((to, from, next) => {
    if (!to.meta.middleware) {
        return next()
    }
    const middleware = to.meta.middleware

    const context = {
        to,
        from,
        next,
        store
    }
    return middleware[0]({
        ...context
    })
})

We first check to see if the current route being processed has a meta field that contains a middleware property. If the middleware property is found, we assign it to a const variable. Next, we define a context object which contains everything we’ll need to pass to each of the middleware. We then call the very first middleware in the middleware array as a function while passing in the context object.

Try visiting the /dashboard route, you should be redirected to the login route. This is because of the store.state.user.loggedIn property in our /src/store.js is set to false. Change the store.state.user.loggedIn property to true and you should be able to access the /dashboard route now.

Our middleware is working now, but not quite the way we want it to. Our goal was to achieve a pipeline where we can run multiple middlewares against a particular route.

return middleware[0]({ …context})

Notice this line of code from the code block above, we’re only calling the first piece of middleware passed from the middleware array in the meta field. How then do we ensure other middlewares contained in the array, if any, are called too? This where our pipeline comes in handy.

Creating the pipeline

Navigate to the src/router directory and then create a middlewarePipeline.js file. Add the following code to the file:

function middlewarePipeline (context, middleware, index) {
    const nextMiddleware = middleware[index]

    if(!nextMiddleware){
        return context.next 
    }

    return () => {
        const nextPipeline = middlewarePipeline(
            context, middleware, index + 1
        )

        nextMiddleware({ ...context, next: nextPipeline })

    }
}

export default middlewarePipeline

The middlewarePipeline takes in three arguments:

context: This is the context object we created earlier on so that it can be passed to each piece of middleware in the stack.

middleware: This is the middleware array itself as defined on the meta field of the route.

index: This is the index of the current middleware being run in the middleware array.

const nextMiddleware = middleware[index]
if(!nextMiddleware){
return context.next
}

Here, we simply pluck out the middleware at the index that was passed to the middlewarePipeline function. If no middleware is found at that index , the default next callback is returned.

return () => {
const nextPipeline = middlewarePipeline(
context, middleware, index + 1
)
nextMiddleware({ ...context, next: nextPipeline })
}

We invoke the nextMiddleware to be run while passing in the context and then the nextPipeline const . It is important to note that the middlewarePipeline function is a recursive function that will call itself to fetch the next middleware to run in the stack, while incrementing the indexby 1.

Bringing it all together

Let’s make use of our middlewarePipeline . Edit the src/router/router.jsfile with the following code:

import Vue from 'vue'
import Router from 'vue-router'
import store from '../store'

import Login from '../components/Login'
import Dashboard from '../components/Dashboard'
import Movies from '../components/Movies'

import guest from './middleware/guest'
import auth from './middleware/auth'
import isSubscribed from './middleware/isSubscribed'
import middlewarePipeline from './middlewarePipeline'


Vue.use(Router)

const router = new Router({
    mode: 'history',
    base: process.env.BASE_URL,
    routes: [{
            path: '/login',
            name: 'login',
            component: Login,
            meta: {
                middleware: [
                    guest
                ]
            }
        },

        {
            path: '/dashboard',
            name: 'dashboard',
            component: Dashboard,
            meta: {
                middleware: [
                    auth
                ]
            },
            children: [{
                path: '/dashboard/movies',
                name: 'dashboard.movies',
                component: Movies,
                meta: {
                    middleware: [
                        auth,
                        isSubscribed
                    ]
                }
            }],
        }
    ]
})

router.beforeEach((to, from, next) => {
    if (!to.meta.middleware) {
        return next()
    }
    const middleware = to.meta.middleware

    const context = {
        to,
        from,
        next,
        store
    }


    return middleware[0]({
        ...context,
        next: middlewarePipeline(context, middleware, 1)
    })

})

export default router

Here, we’re making use of the middlewarePipeline to run subsequent middlewares contained in the stack.

return middleware[0]({
...context,
next: middlewarePipeline(context, middleware, 1)
})

After the first middleware has been called, using the middlewarePipeline function, subsequent middlewares contained in the stack are also called, till there are no longer any middlewares available.

If you visit the /dashboard/movies route, you should be redirected to the /dashboard route. This is because the user is currently authenticated but has no active subscription. Set the store.state.user.isSubscribed property in our store to true . You should be able to access the /dashboard/moviesroute now.

Conclusion

Middlewares are a great way of protecting different routes in an application. This is a very simple implementation of how you can make use of multiple middlewares to protect a single route in your Vue application. You can find the link to the Github repo here.

 

Plug: , a DVR for web apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.

.
Dotun Jolaoso Software Developer

8 Replies to “Understanding Vue middleware pipelines”

  1. Interesting pattern as a first check, but since you’ll be making authenticated API requests to the backend to fetch data for these protected routes, which could fail if they’re not logged in or don’t have appropriate permissions, it seems redundant to also have a front-end middleware checking against the local VueX store. The redirects or error message display could be wrapped into an ApiClient class instead when the front-end gets an unauthorized error?

  2. Hello Dotun,
    Thanks for the a the blog, it was very insightful. Is there any way to apply middleware to all children routes instead of defining middleware to each route??

  3. Hello,

    Unfortunately, next code is not working:

    return middleware[0]({
    …context,
    next: middlewarePipeline(context, middleware, 1)
    })

    middleware[0] is not a function.

  4. How to pass more variable to middleware. Something like this

    meta: {
    middleware: [
    role(‘admin’)
    ]
    }

    And then I can check the user can enter or not a page via role

Leave a Reply