Published 1/15/2024 · 4 min read
Tags: vue , middleware , routing , typescript , patterns
As your app grows, you’ll need a way to control what happens before a route loads. The basic approach using beforeEach with meta.requiresAuth works well for simple authentication checks, but what happens when you need multiple checks? A user might need to be authenticated AND be an admin to access certain routes.
The Middleware Pattern
Instead of cramming all logic into a single beforeEach hook, we can use the middleware design pattern. This lets us chain multiple middleware functions together while keeping the router code clean.
Setting Up Middleware with Vue 3 and Pinia
First, let’s define the types. Create src/middleware/types.ts:
import type { RouteLocationNormalized, NavigationGuardNext } from "vue-router";
export interface MiddlewareContext {
to: RouteLocationNormalized;
from: RouteLocationNormalized;
next: NavigationGuardNext;
}
export type Middleware = (context: MiddlewareContext) => void | Promise<void>;
The Middleware Pipeline
Create src/middleware/pipeline.ts:
import type { MiddlewareContext, Middleware } from "./types";
export function middlewarePipeline(
context: MiddlewareContext,
middleware: Middleware[],
index: number
): () => void {
const nextMiddleware = middleware[index];
if (!nextMiddleware) {
return context.next;
}
return () => {
nextMiddleware({
...context,
next: middlewarePipeline(context, middleware, index + 1),
});
};
}
Breaking down the middlewarePipeline function:
- Parameters - Receives the context, middleware array, and current index
- Get next middleware - Retrieves the next middleware function from the array
- Base case - If no more middleware exists, return the original
nextfunction to load the route - Recursive call - Returns a function that calls the next middleware, passing in context with an updated
nextthat points to the following middleware
Router Configuration
Update src/router/index.ts:
import { createRouter, createWebHistory } from "vue-router";
import { middlewarePipeline } from "@/middleware/pipeline";
import type { Middleware } from "@/middleware/types";
import auth from "@/middleware/auth";
import guest from "@/middleware/guest";
import admin from "@/middleware/admin";
// Extend route meta type
declare module "vue-router" {
interface RouteMeta {
middleware?: Middleware[];
}
}
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: "/",
name: "home",
component: () => import("@/views/HomeView.vue"),
},
{
path: "/login",
name: "login",
component: () => import("@/views/LoginView.vue"),
meta: { middleware: [guest] },
},
{
path: "/register",
name: "register",
component: () => import("@/views/RegisterView.vue"),
meta: { middleware: [guest] },
},
{
path: "/dashboard",
name: "dashboard",
component: () => import("@/views/DashboardView.vue"),
meta: { middleware: [auth] },
},
{
path: "/settings",
name: "settings",
component: () => import("@/views/SettingsView.vue"),
meta: { middleware: [auth] },
},
{
path: "/users",
name: "users",
component: () => import("@/views/UsersView.vue"),
meta: { middleware: [auth, admin] },
},
],
});
router.beforeEach((to, from, next) => {
const middleware = to.meta.middleware;
// No middleware required, continue
if (!middleware || middleware.length === 0) {
return next();
}
const context = { to, from, next };
// Start the middleware pipeline
middleware[0]({
...context,
next: middlewarePipeline(context, middleware, 1),
});
});
export default router;
How It Works
When a user navigates to /users, here’s what happens:
- Router intercepts -
beforeEachcatches the navigation - Middleware array found -
[auth, admin]from route meta - First middleware runs -
authchecks authentication - Pipeline continues - If
authcallsnext(), the pipeline runsadmin - Admin check -
adminverifies the user has admin privileges - Route loads - If both pass, the original
next()is called
The key insight is that each middleware controls whether to continue by calling next(). If a middleware doesn’t call next() (or redirects instead), the pipeline stops.
Benefits of This Pattern
- Separation of concerns - Each middleware handles one responsibility
- Reusability - Middleware functions can be combined in different ways
- Testability - Each middleware can be unit tested independently
- Type safety - TypeScript ensures correct context passing
- Clean routes - Route definitions clearly show their requirements
Next up: Building the auth middleware function.
Related Articles
- Routing and Pages
Master SvelteKit's file-based routing system. Learn dynamic routes, route groups, optional parameters, and layout patterns.
- Form Handling: Moving from Vue to Svelte
A practical guide to translating Vue form patterns to Svelte, covering two-way binding, validation, async submission, and what actually works better in each framework.
- Building a Modal: Vue vs Svelte
A side-by-side comparison of building a modal component in Vue 3 and Svelte 5, exploring the differences in reactivity, props, and component patterns.