Building a Saas App with Vue.js
Six months ago I started working on a my first real Saas app called This post is a snapshot of five learnings that stuck with me over the last half year
I hope it helps other self-starting solo developers that have embraced the gooey warmth and bliss that is Vue.js.
To give you some context, this is what Checkly does in a nutshell. Checkly is a tool for IT Ops and Developer people. It does two things.
- It actively validates the correctness and response time of API end points.
- It actively validates the correctness of critical browser click flows.
Enough sales talk. These are the five things that I learned.
1. Making authentication not suck is hard.
Users will probably interact more with your login page than with your landing page, pricing page or some other irrelevant part of your app that only YOU think is really really cool. That’s why login and its related authentication buddies should not suck.
In a typical and non-trivial authentication scenario you have multiple hoops you have to jump through:
- Social login
- Username / password login
- Password reset
- Callback handler for social login
- Register / first signup handler
That is already a fair amount of complexity and a lot of things that need to go right before your user can actually start doing stuff. Also, testing of these components can be tricky, as there is a lot of backend and third-party service interaction going on that is hard to mock out or otherwise work around.
In my solution, I took two specific architectural decisions to ease this pain:
- Use Auth0 for the actual authentication backend. I use the social login, username / password login and their JWT service to authenticate the user to the Checkly API. Disclaimer: I have zero point zero affiliation with Auth0, but their service is top notch and very 💵 friendly to startups due to their very liberal free plan.
- Abstract Auth0 interaction into a service component in the Vue app. Your Vue components have no knowledge of Auth0, JWT tokens of anything. They just import an “auth” service and call some methods. Easy to swap out later if needed.
- Separate each “step” into its own route and component. This makes your life and other peoples’ lives easy. You can now easily throw around /login and /signup links in your marketing without worrying about component state. The state is determined by the route, like any normal web page.
Here’s a visual representation of the whole setup. The dancing hamster means you fully signed up and/or logged in.
Bonus: handling unauthorized states
When a user’s JWT token expires, the API backend will say so by responding with a 401 Unauthorised header. You probably want to redirect the user to your login page or show some friendly message. Using the Axios http client and Vue this is about five lines of code.
- Add an interceptor to Axios in your central http.js service. Have Axios emit an error on a Vue.js message bus.
- Listen for the message in your top component and take action. In my case a redirect to /login.
2. Component reuse requires a little planning.
What is a component? A button can be a component. But also a full screen can be a component. Your whole app is a component! Turns out, the split between “components” and “not-components” is not really fine grained enough when making sense of how an app is built.
Of course, I’m not the only one who stumbled onto that. Dan Abramov ‘s article, in a ReactJS context of course, kinda sums it up: split components into containers and things you look at (representational components in Dan’s post, views in mine). The only thing really specific to my solution is that views are always mapped to sub routes.
In the example below you see a typical edit screen. In this case an edit screen for editing an API check. This is the container and it maps to url
/checks/<id>/edit/apiThe screen is on the “locations & scheduling” tab, which is mapped to its own sub route at
This tab contains the country selection and time scheduling components (the red ones). However, these components also live in three other screens: two times in a create wizard and one time in the edit screen for browser checks.
Each of these screens is different and provides a different data context and visual context for each of the components, like whether we are in “edit” mode or “create” mode. In other words, not only the data injected into the component changes (via props), but also the visual arrangement on the screen. Using the containers / views split helps organise this.
- Containers are top level routes and most of the time read from Vuex.
- Views are explicitly sub routes of containers and hold no state.
- Both can hold bare components.
3. Deploying to a CDN also requires a little planning.
And now for something completely different!
The Checkly backend is deployed on Heroku, so I looked into serving the web app also from Heroku. The fewer moving parts the better! However, this is more hassle than you would think.
- You need to sync web app deploys with the API deploys. Not very practical.
- You need to pack the compiled assets in the API deployable. In most cases, these are separate (git) projects. Now you need to add a stage where artefacts from one are injected to the other and 🤢
I looked at third party deployment/CI/CD services. I looked at Netlify. But in the end, I just used some simple NPM scripting together with the knowledge I already had from AWS.
Whip up an S3 bucket and just install the s3-deploy NPM package. Add three lines in the package.json scripts section.
- Deploy all assets to an S3 bucket and set caching headers and etags.
- Deploy your index.html with no caching headers, effectively cache busting your browser’s cache whenever there is a new version.
- Chain both together. Done
This set of scripts pushes a Vue.js app that was initially started with Vue-CLI to an AWS S3 bucket. In my situation, I also fronted the S3 bucket with AWS Cloudfront as a CDN. There are a couple of initial setup gotcha’s when creating this setup:
- Add a robots.txt in your web pack config. Gotta tame those bots.
- Already mentioned, but still: take care of proper caching headers. You want every code update or asset update to be instantly visible. This means you should push your index.html with ZERO caching. This is in general a good practice.
- Set up AWS S3 and AWS Cloudfront to handle a single page app. This is almost completely absent from AWS’s own documentation.
- Set the bucket to hosting
- Set the error page to index.html
- Create custom error responses on Cloudfront for 404 AND 403 messages.
4. Embrace third-party plugins, but only when…
Time out. I’ve been in IT for about 20 years now. I have nerdy war stories about the first internet bubble. But as much as technology, the market and the world has changed, one thing has never changed
DEVELOPING NICE AND USEFUL APPLICATIONS IS A SH*T TON OF WORK
And everyone STILL underestimates this: managers, engineers, customers, me. Therefore, the sole reason for me to use third party solutions, open source frameworks, plugins etc. is whether I can reduce the amount of work. Not to integrate with something fancy, not to be the latest to use x or y. Not because Facebook uses it. Seems totally obvious, but many people seem to forget.
Rant over. Here are three Vue.js plugins I can recommend. I use them heavily. What they do is probably clear. They are not perfect, but they will help you reduce your workload.
5. Vue.js dev tools is a fantastic time saver.
The Vue.js dev tools Chrome extension is incredible. I use it every day. For me, it is an integral part of the Vue.js eco system, like Vuex and Vue Router. Here’s an example of why it rocks.
I’m fairly sensitive to UI and UX concerns. I don’t mind spending a lot of time getting
- A color that pops.
- A transition that smooths things out.
- A micro animation that provides context.
- A font type that sets you apart.
This means that in error situations, or other corner cases, I still want something nice to happen. Making this work during development for the frontend can be quite tricky.
Toggling true/false states with quick edit really, really REALLY helps when transitions and stuff depend on complex backend states and XHR messages.
In the example above, the backend API needs to be in a specific state to trigger a certain screen on the frontend. However, that state is not static: it depends on time and other variables that continuously change.
Short of adding your own debug buttons to toggle these states, there is not much else you can do. Vue.js dev tools takes this pain away. Thanks Vue.js dev tools.