I watched Gary Bernhardt's talk on static routing back a few years ago and — I'm not sure if I would call it formative, but it stuck in my craw as a platonic ideal of sorts, as something I couldn't really justify adopting within Buttondown but really wanted.

I built out and open-sourced some feints in this direction — see django-typescript-routes, which provides a TS router generated from a Django backend — but that's not quite the same thing, and time and time again I found myself in the position of pushing bugs that would have been caught if I had a typesafe router in Vue.

Tanner Linsley makes the pithiest possible case for such an abstraction:

Too many people don't realize they're managing the most critical state of their application in a /string?Record<string, string># string type. 🤦‍♂️

I was thrilled to stumble upon the very poorly named unplugin-vue-router earlier this year and resolved to spend some time hacking with it to see if it was worth the cost. It was, and I'm glad I did it.

How it works

Vue Router is a very simple abstraction: you define a list of routes (where a route is a component and a matching path and some metadata), and Router routes for you. Something like this:

import { createMemoryHistory, createRouter } from "vue-router";

import HomeView from "./HomeView.vue";
import UserListView from "./UserListView.vue";
import UserDetailView from "./UserDetailView.vue";

const routes = [
  { path: "/", component: HomeView },
  { path: "/users", component: UserListView },
  { path: "/users/:id", component: UserDetailView },

const router = createRouter({
  history: createMemoryHistory(),

Nothing particularly magical or fancy. The Faustian bargain you sign with UVR, though, is that in order to get typesafe routing you must also adopt file-based routing:

And then the above mapping file gets magicked away:

import { createMemoryHistory, createRouter } from "vue-router/auto";

const router = createRouter({
  history: createMemoryHistory(),

I actually don't mind the file-based routing, but it made adoption much more painful — it was very difficult to do a piecemeal migration, and it basically ended up as an omnibus PR touching every single view in the application. (Though that PR was made much safer by the fact that now all the routes had type information!)

You might also notice that the third file was not /users/[id].vue, but /[users].[id].vue. UVR handles nested routing for things like modals differently than I was used to in Next; you nest modals by plopping them in directories in a way that is logically coherent but still takes a bit of getting used to.

Three months later

By the time I was truly waist-deep in the UVR migration, it felt like it was:

  1. Too late to turn back;
  2. Perhaps not worth all the effort just for some type safety.

Three months later, though, I am quite glad I did it. It was a pretty big up-front cost, but has saved me many times over from pushing bad code, and the doubts I had about the approach being 'janky' and messing with VSCode have not borne out.

If you're using Vue, highly recommend. (Now all I need to do is get a similar abstraction for query parameters!)

Lightning bolt
Subscribe to my newsletter

I publish monthly roundups of everything I've written, plus pictures of my corgi.
© 2024 Justin Duke · All rights reserved · have a nice day.