Skip to content

Auth and Guards ​

Problem ​

Auth checks duplicated across data loaders create inconsistent enforcement. Unauthenticated users reaching protected routes receive partial or invalid state.

Solution ​

Use middleware on protected routes. Redirect before next() is called to cancel the navigation entirely.

ts
import { createRouter } from '@vielzeug/wayfinder';

const requireAuth = async (ctx, next) => {
  if (!session.currentUser) {
    // Replace the history entry so back-button doesn't loop back to /dashboard.
    await ctx.navigate({ name: 'login' }, { replace: true });
    return;
  }

  ctx.locals.user = session.currentUser;
  await next();
};

const router = createRouter({
  routes: {
    login: {
      path: '/login',
    },
    dashboard: {
      path: '/dashboard',
      middleware: [requireAuth],
    },
  },
  notFound: { data: () => ({ message: 'Not found' }) },
});

With redirectTo() helper ​

For simple unconditional redirects, use the redirectTo() helper instead of a manual middleware function.

ts
import { createRouter, redirectTo } from '@vielzeug/wayfinder';

const router = createRouter({
  routes: {
    login: { path: '/login' },
    admin: {
      path: '/admin',
      middleware: [redirectTo({ name: 'login' }, { replace: true })],
    },
  },
});

Pitfalls ​

  • Middleware that neither calls next() nor redirects cancels the navigation silently — the router stays on the current URL and status returns to 'idle'. This is intentional for blocking middleware.
  • Per-route middleware runs after global middleware, so any auth state populated by a global middleware loader is already available in ctx.locals.
  • redirectTo() is unconditional. For conditional guards (e.g., checking a role), write a custom middleware function.