Structuring a Vue project — Authentication
For the past few years my primary focus has been on the software architecture and development of the backend services. I’ve tried to stay away from front-end as long as possible since it’s the one area of software development where I feel mostly useless and unproductive.
So with the wish to sharpen my skills a bit I’ve decided to dig a bit into front-end development and see how it plays out. I’ve done some Angular quite a few years back so I thought I’d try the latest version but since Vue is growing in popularity and we’ve recently started using it at our company I’ve decided to give it a go. There are some quite long code snippets here, as I tried to structure this post more in a tutorial fashion — I hope you’ll still make it to the end :)
Generally when starting to work in a new framework or language I try to lookup as many best practices as possible as I prefer to start with a good structure that can be easily understood, maintained and upgraded in the future. In this post I will try to explain my thinking and combine all the knowledge I’ve obtained in the last few years with the latest and greatest web development practices.
Together we will build a simple project that handles authentication and prepare basic scaffolding to use when building the rest of the app.
We’ll be using:
- Vue.js 2.5 and Vue-CLI
- Vuex 3.0
- Axios 0.18
- Vue Router 3.0
Here is the end project structure that we’ll come up with when all is said and done. I assume that you’ve read through the Vue, Vuex and Vue Router documentation and that you understand the basics behind it — if you didn’t don’t panic, I’ll keep things simple, just don’t expect to learn everything from this post ;)
│ └── logo.png
│ └── HelloWorld.vue
│ ├── api.service.js
│ ├── storage.service.js
│ └── user.service.js
│ ├── auth.module.js
│ └── index.js
First let’s protect some of the URLs to be accessible only to logged in users. To do this we need to edit router.js. I’ve taken the approach where all the pages are private except the ones we mark as public directly as I believe it’s better to set the visibility by default to private and expose the routes you want accessible to public by explicitly doing so.
In the code below we’re using the meta functionality of the Vue Router — you can read about it here. We’ll also be nice to our users and redirect them to the page they tried to access after the login. For the login view we kinda want it to be accessibly only when a user is not logged in so we’ve added another flag in the meta field named onlyWhenLoggedOut.
You’ll notice that we’re importing a TokenService which returns a token. The TokenService lives inside services/storage.service.js and all it does is encapsulate the logic that handles the storage and retrieval of the access token to and from localStorage.
This way we can safely migrate from local storage to cookies without having to worry that we’ll break some other service or component that accesses the local storage directly. I believe this is a good practice to follow in order to avoid future headaches. The code in the storage.service.js looks like this:
Making API requests
We can use the same logic as we did in TokenService when it comes to API interaction. Make a base service which will do all the interaction with the network so we can easily change or upgrade stuff in the future. This is exactly what we’re trying to achieve with the api.service.js— encapsulate the Axios library so that when inevitably a new thing comes along we can return to this single service and upgrade it without having to refactor the whole application. Any other service that needs to interact with API will simply import the ApiService and issue requests via the helper methods we’ve implemented.
You might have noticed that there is an init and setHeader function in there. We will init the ApiService inside the main.js to make sure that we set the header if the user refreshes the page and also set the baseURL property.
To dynamically change the URL in development, staging and production environment I’m using the Vue CLI Environment Variables.
Inside your main.js file you’d place appropriate imports and then following lines:
Okay, so by now we know how to redirect the user to a login page and we’ve done some basic boilerplate code that should help us keep a clean and maintainable project. Let’s start working on the user.service.js so we can actually make a request and see how to use our ApiService we just made.
We’re implementing a UserService which has 3 methods:
- login — prepare a request and obtain a token from the API via the API service
- logout — clear user stuff from the browser storage
- refresh token — obtain a refresh token from the API service
If you’ve paid attention you’ll notice that there is a mysterious 401 interceptor logic there — we’ll cover that in a moment. It’s there just so I don’t have to include another code snippet to show you where to place it.
Should I place it in Vuex store or Component?
It seems to be a good practice to put as much of your logic into the Vuex store. Primarily this is good because you can reuse the state and business logic in different components.
For example let’s say that your app allows users to login or register in multiple places — on a dedicated login/register page or, in case of a online shop, when they checkout their basket via a popup. You would probably use a different Vue component for that UI element. By placing your state and logic in the Vuex store you would be able to reuse the state and logic and simply make a few short import statements in your Component like this:
In your Vue Component you would import logic from the Vuex Store and map state or getters to your computed values and actions to your methods. You can read a bit more about mapping here.
So how does our Vuex store look like for the user.service.js?
This pretty much covers everything that you need to setup your project in a way that should hopefully help you keep things clean and maintainable.
Fetching more data from API should be easy now — simply make a new <something>.service.js inside services, write helper methods and access API via the ApiService we made. To display this data make a Vuex Store and store the API responses in the state — use it in your Component via the mapState and mapActions. This way you’ll be able to reuse the logic in the future should you need to display or manipulate the the same data in a different component.
While I’m not a Vue expert (yet) I do like to think I know a thing or two about software architecture and I hope that this post contains some useful ideas and concepts that you can use in your next project!
Extra: How to refresh expired access token?
What’s a bit harder and skipped by so many tutorials when it comes to authentication is handling token refresh or 401 errors. There are some use-cases where it’s good to simply logout the user when a 401 error happens, but let’s see how you can refresh the access token without interrupting the user experience. So here is the 401 Interceptor we’ve had in the above code samples mentioned already.
In our ApiService we’ll add the following code to mount the Axios response interceptor.
What the code above does is intercept every API response and check if the status of the response is 401. If it is, we’re checking if 401 occurred on the token refresh call itself (we don’t want to be caught in the loop of refreshing token forever!). The code then refreshes the token and retries the request that has failed and returns the response back to the caller.
We’re dispatching a call to the Vuex store here to perform the token refresh. The code we need to add to our auth.module.js is:
You app will probably perform several API requests to obtain the data it needs to display. Should the access token expire all of the requests will fail and therefore trigger the token refresh inside the 401 interceptor. That would in term refresh the token for each request and that is not a good thing.
There are solutions out there that queue the requests when 401 happens and process them in a queue, but the code above, at least to me, provides a more elegant solution. By saving the refresh token promise and returning the same promise to every refresh token request we’re making sure that the token gets refreshed only once.
You’ll also need to mount the 401 interceptor in your main.js as well right after you set the request header.
PS: You could simply check on page load the expiry time and refresh token then as well, but that does not work for long lived sessions where user simply does not refresh your page.