Vercel Logo

Route Protection

Right now, anyone can visit /favorites and /visited. They'll see empty pages, but they shouldn't be there at all without logging in. We need a guard.

In Next.js, you'd create a middleware.ts file at the project root that runs on every request, checking cookies or sessions and redirecting. It operates at the edge, before the page even starts rendering. Powerful, but it's a single file that handles ALL route protection with conditional logic.

Nuxt middleware is per-page. You create a middleware function, then opt individual pages into it with definePageMeta. No global file, no conditional URL matching. Each page declares its own protection. It's more explicit and less likely to accidentally lock someone out of the wrong page.

Outcome

Create an auth middleware and apply it to the favorites and visited pages.

Fast Track

  1. Create app/middleware/auth.ts that checks useUserSession and redirects
  2. Add definePageMeta({ middleware: "auth" }) to the favorites and visited pages
  3. Verify unauthenticated users get redirected to login

Hands-on exercise 3.3

Build the middleware and protect the authenticated pages.

Requirements:

  1. Create app/middleware/auth.ts that redirects to /login if the user isn't logged in
  2. Add definePageMeta({ middleware: "auth" }) to app/pages/favorites.vue
  3. Add definePageMeta({ middleware: "auth" }) to app/pages/visited.vue
  4. Verify unauthenticated users get redirected when visiting either page

Implementation hints:

  • defineNuxtRouteMiddleware creates a named middleware. The filename becomes the name
  • useUserSession() works inside middleware because middleware runs in the Nuxt context
  • navigateTo("/login") returned from middleware triggers a redirect
  • definePageMeta is a compiler macro like defineProps. It runs at build time, not runtime

Here's the middleware:

app/middleware/auth.ts
export default defineNuxtRouteMiddleware((to) => {
  const { loggedIn } = useUserSession();
 
  if (!loggedIn.value) {
    return navigateTo("/login");
  }
});

Four lines. Check if logged in, redirect if not. The to parameter gives you the target route if you need conditional logic, but our middleware is straightforward: not logged in, go to login.

In Next.js, the equivalent would be in middleware.ts:

// Next.js middleware.ts — for comparison
import { NextResponse } from "next/server";
 
export function middleware(request) {
  const session = request.cookies.get("session");
  if (!session) {
    return NextResponse.redirect(new URL("/login", request.url));
  }
}
 
export const config = {
  matcher: ["/favorites", "/visited"],
};

Same idea, different ergonomics. Next.js uses a matcher config to specify which routes the middleware applies to. Nuxt puts that decision on the page itself.

Now apply the middleware to the pages that need it. Add definePageMeta to the favorites page:

app/pages/favorites.vue
<script setup lang="ts">
definePageMeta({
  middleware: "auth",
});
</script>

And the visited page:

app/pages/visited.vue
<script setup lang="ts">
definePageMeta({
  middleware: "auth",
});
</script>

definePageMeta is a compiler macro, like defineProps. It gets extracted at build time and doesn't appear in the compiled component. The string "auth" matches the filename auth.ts in the middleware directory. Nuxt connects them automatically.

You can stack multiple middleware on a page with an array: middleware: ["auth", "admin"]. They run in order. If any middleware returns a redirect, the chain stops.

Global vs named middleware

Our auth.ts is a named middleware, applied per-page. If you want middleware that runs on EVERY page, create it in app/middleware/ with a .global.ts suffix, like auth.global.ts. We don't want that here because the browse page and login page should be public.

definePageMeta runs at build time

You can't use runtime variables inside definePageMeta. No definePageMeta({ middleware: someCondition ? "auth" : undefined }). The value must be static. If you need conditional middleware, put the condition inside the middleware itself.

Try It

  1. Log out if you're currently logged in
  2. Visit http://localhost:3000/favorites directly. You should be redirected to /login
  3. Visit http://localhost:3000/visited. Same redirect
  4. Visit http://localhost:3000/springs. No redirect. The browse page is still public
  5. Log in via GitHub. Visit /favorites and /visited. Both should load normally

Commit

git add -A && git commit -m "feat(auth): add route middleware to protect favorites and visited pages"

Done-When

  • app/middleware/auth.ts redirects unauthenticated users to /login
  • /favorites and /visited require authentication
  • /springs and /login remain public
  • You can explain the difference between named middleware and global middleware in Nuxt

Solution

app/middleware/auth.ts
export default defineNuxtRouteMiddleware((to) => {
  const { loggedIn } = useUserSession();
 
  if (!loggedIn.value) {
    return navigateTo("/login");
  }
});

Add definePageMeta to both protected pages:

app/pages/favorites.vue
<script setup lang="ts">
definePageMeta({
  middleware: "auth",
});
</script>
 
<template>
  <div>
    <h1>
      Your Favorites
    </h1>
    <p>
      Saved favorites will appear here once we add user features.
    </p>
  </div>
</template>
app/pages/visited.vue
<script setup lang="ts">
definePageMeta({
  middleware: "auth",
});
</script>
 
<template>
  <div>
    <h1>
      Visited Springs
    </h1>
    <p>
      Your visited springs will appear here once we add tracking.
    </p>
  </div>
</template>