What is a middleware pipeline?

Typically, when you build a SPA, you need to secure some routes. For example, if we had a Dashboard route that only authenticated users could access, we could use Auth middleware to ensure that legitimate users could access it.

A middleware pipeline is a collection of different pieces of middleware that run in parallel with each other.

Continuing the previous example, suppose there is another route on /dashboard/movies that we only want subscribers to access. We already know that to access the Dashboard route, you need to authenticate. So how do you secure /dashboard/movies routes to ensure that only authenticated and subscribed users can access them? By using middleware pipelines, you can link multiple middleware together and ensure that they run in parallel.

start

Start by quickly building a new Vue project with the Vue CLI.

vue create vue-middleware-pipeline
Copy the code

Install dependencies

Once the project directory is created and installed, switch to the newly created directory and run the following command from your terminal:

npm i vue-router vuex
Copy the code

Create components

Our program will consist of three components.

Login – This component is displayed to users who have not been authenticated.

Dashboard – Magnetic components displayed to logged in users.

Movies – We display this component to users who are logged in and have valid subscriptions.

Let’s create these components. Switch to the SRC/Components directory and create the files: login. vue, dashboard. vue, and movies.vue.

Use the following code to edit the login. vue file:

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

Use the following code to edit the dashboard.vue file:

<template>
    <div>
        <p>This is the Dashboard component</p>
    </div>
</template>
Copy the code

Edit the movies. vue file with this code:

<template>
    <div>
        <p>This is the Movies component</p>
    </div>
</template>
Copy the code

Create the store

As far as Vuex is concerned, store is just a container for holding the state of our program. It allows us to determine whether the user is authenticated and to check whether the user is subscribed.

In the SRC folder, create a store.js file and add the following code to it:

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
        }
    }
})
Copy the code

A store contains a User object in its state. The user object contains the loggedIn and isSubscribed attributes, which help us determine whether the user is loggedIn and has a valid subscription. We also define a getter in the store to return the User object.

Define the routing

Before you create routes, you should define them and associate the corresponding middleware to be attached to them.

Everyone except authenticated users can access /login. When authenticated users access this route, they should be redirected to the/Dashboard route. This route should be accompanied by a guest middleware.

Only authenticated users can access/Dashboard. Otherwise, users should be redirected to the /login route when accessing this route. We associate auth middleware with this route.

Only authenticated and subscribed users can access /dashboard/movies. The routing is protected by isSubscribed and Auth middleware.

Create routing

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

import Vue from 'vue'
import Router from 'vue-router'

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
Copy the code

Here, we create a new Router instance, passing several configuration options and a Routes attribute that accepts all the routes we defined earlier. Note that these routes are currently unprotected.

Next, inject the Router and Store into the Vue instance. Edit the SRC /main.js file with the following code:

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')
Copy the code

Creating middleware

Create a middleware folder in the SRC/Router directory, and create guest.js, auth.js, and isSubscribed. 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()
}
Copy the code

The guest middleware checks whether the user is authenticated. If authenticated, it is 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()
}
Copy the code

In auth middleware, we use store to check if the user is currently logged in, and we either continue the request or redirect it to the login page.

Use the following code to edit the isSubscribed.js file:

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

The middleware in isSubscribed is similar to the Auth middleware. We also check the bucket store to see if the user subscribes. If the user is subscribed, they can access the expected route, otherwise they are redirected back to the Dashboard route.

Protect the routing

Now that you’ve created all the middleware, let’s leverage it to secure the route. 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
Copy the code

Here, we import all the middleware, and then define a meta-field containing the middleware array for each route that needs to be secured. The middleware array contains all the middleware we want to associate with a particular route.

Vue route navigation guard

We use the navigational guard provided by the Vue Router to protect the route. These navigators protect routes by redirecting or canceling them.

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

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

The beforeEach method receives three arguments:

To: This is the route we intend to access.

From: This is our current route.

Next: This is the function that calls the hook.

Running middleware

Run our middleware using the beforeEach hook.

router.beforeEach((to, from, next) = > {
    if(! to.meta.middleware) {return next()
    }
    const { middleware } = to.meta
    const context = {
        to,
        from,
        next,
        store
    }
    return middleware[0] ({... context }) })Copy the code

We first check that the route we are currently processing has a meta-field containing the Middleware property. If you find the Middleware property, deconstruct it. Next we define a Context object that contains everything we need to pass to each middleware. The first middleware in the middleware array is then called as a function, passing in the context object.

Try to access the/Dashboard route and you will be redirected to the /login route. This is because the SRC/store. The store in the js. State. The user. The loggedIn attribute is set to false. Change it to true to access the/Dashboard route.

Now the middleware is running, but it’s not the way we want it. Our goal is to implement a pipeline that can run multiple middleware for a particular route.

return middleware[0] ({... context })Copy the code

Notice that in this line of code in the block above, we only call the first middleware passed from the middleware array in the META field. So how do we ensure that any other middleware protected in the array (if any) is also called? That requires pipes.

Create the pipe

Switch to the SRC/Router directory and create a middlewarePipeline.js file. Add the following code to the file:

export default 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 })
    }
}
Copy the code

MiddlewarePipeline takes three parameters:

Context: This is the context object we created earlier, which can be passed to each middleware in the stack.

Middleware: This is the Middleware array defined on the Meta field of the route.

Index: This is the index of the current middleware running in the Middleware array.

const nextMiddleware = middleware[index]

if(! nextMiddleware) {return context.next
}
Copy the code

In this case, we simply look for the corresponding middleware based on index, and if none is found, the default next callback is returned.

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

We call nextMiddleware to pass the context, then pass nextPipeline. It is worth noting that the middlewarePipeline function is a recursive function that calls itself to get the next middleware to run on the stack, incrementing the index by 1.

Put them together

Let’s use middlewarePipeline. Edit the SRC /router/router.js file like this:

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
    const context = {
        to,
        from,
        next,
        store
    }
    return middleware[0] ({... context,next: middlewarePipeline(context, middleware, 1)})})export default router
Copy the code

Here, we use the middlewarePipeline to run the included subsequent middleware recursively.

If you access the /dashboard/movies route, you are redirected to /dashboard. This is because the user is currently authenticated but has no valid subscription. If you set the isSubscribed property in the store to true, you can access the /dashboard/movies routes.

conclusion

Middleware is a good way to protect different routes in an application. This is just a very simple demo. There may be more complex scenarios to consider in the actual application, but the ideas can be referenced.