Vue Router on a Laravel API

Routes, a login redirect, and a guest guard for a Vue app that is not Inertia. Load the user before the router runs.

For your AI agent

Copies a prompt. Paste it into the AI agent you already have open.

Read https://laravel-vuejs.space/vue-router-laravel-spa.md
Routes, a login redirect, and a guest guard for a Vue app that is not Inertia. Load the user before the router runs.
Show the diff. Do not commit.

Next: Nuxt 4 in front of a Laravel API

On this page · 7 sections
  1. The routes
  2. Load the user first
  3. After login
  4. Recap
  5. FAQ
  6. Next steps
  7. Sources

You log in, refresh, and the router kicks you back to /login even though the session is fine. The guard ran before the app knew who you were. Here is Vue Router for a separate Laravel SPA, with the user loaded before the first navigation.

TL;DR

  • Inertia apps do not use Vue Router. This is for a separate SPA only.
  • Mark routes with meta.auth / meta.guest; redirect in router.beforeEach.
  • Await GET /api/user (credentialed client) before app.use(router) and mount.
  • Your SPA host must serve index.html for unknown paths.

What you'll learn

  • When Vue Router belongs in a Laravel project (and when it does not)
  • How to define guest and auth routes
  • How to load the user before the first guard runs

Who it's for: Developers building a standalone Vue SPA on a Laravel API

Applies to: Vue Router 4 · Vue 3 · Pinia · Laravel 13 · Sanctum

Heads up: not for the starter kit

This router belongs in a Vue app that talks to Laravel over HTTP. It does not belong in the Vue starter kit. Inertia already changes the page. A second router fights that navigation, and the kit’s URLs stay in routes/web.php. If you are on the kit, stop here and use Laravel and Vue in 2026.

The separate app is the case Sanctum’s cookie flow is for. Axios, the CSRF cookie, and withCredentials stay in Authenticate a Vue SPA with Laravel Sanctum. This page only adds the URLs. Client-only state inside an Inertia app is Pinia, which is a different file than the store below.

The routes#

bash
npm install vue-router

Vue Router 4 is createRouter and createWebHistory. Two routes are enough to see the guard: /login is for guests, and / requires a user.

src/router.tstypescript
import { createRouter, createWebHistory } from "vue-router";
import Login from "./pages/Login.vue";
import Posts from "./pages/Posts.vue";

export const router = createRouter({
  history: createWebHistory(),
  routes: [
    {
      path: "/login",
      name: "login",
      component: Login,
      meta: { guest: true },
    },
    {
      path: "/",
      name: "posts",
      component: Posts,
      meta: { auth: true },
    },
  ],
});

Gotcha: refresh = 404

The host that serves this SPA has to answer unknown paths with index.html. Otherwise a refresh on /login is a 404. The deploy guide is the second host for that SPA. It does not set this fallback.

Load the user first#

router.beforeEach that runs before you know who is logged in sends a valid session to /login on the first paint. Call GET /api/user on the credentialed api client from the Sanctum guide and wait. A plain Axios import does not send the session cookie to another origin. A 401 means there is no session, so the store user stays null. Any other error is a failed API, not a logged-out user.

src/stores/auth.tstypescript
import { defineStore } from "pinia";
import { ref } from "vue";
import axios from "axios";
import { api } from "../api";

export const useAuth = defineStore("auth", () => {
  const user = ref(null);

  async function load() {
    try {
      user.value = (await api.get("/api/user")).data;
    } catch (error) {
      if (axios.isAxiosError(error) && error.response?.status === 401) {
        user.value = null;
        return;
      }

      throw error;
    }
  }

  return { user, load };
});

Mount only after that call. The guard is registered before mount, so the first navigation already knows the answer.

src/main.tstypescript
import { createApp } from "vue";
import { createPinia } from "pinia";
import App from "./App.vue";
import { router } from "./router";
import { useAuth } from "./stores/auth";

async function boot() {
  const app = createApp(App);
  const pinia = createPinia();
  app.use(pinia);

  const auth = useAuth();
  await auth.load();

  router.beforeEach((to) => {
    if (to.meta.auth && !auth.user) {
      return { name: "login" };
    }

    if (to.meta.guest && auth.user) {
      return { name: "posts" };
    }
  });

  app.use(router);
  app.mount("#app");
}

boot();

After login#

The login POST itself is the Sanctum article: CSRF cookie, then credentials, with credentials included. That POST returns no content. Load the account with currentUser() from that same client, write it into the store, and move.

Full reload, store lost, then Router navigation:

src/pages/Login.vuetypescript
window.location.assign("/");
src/pages/Login.vuetypescript
auth.user = await currentUser();
await router.push({ name: "posts" });

Do not window.location assign. The router is the reason this page exists.

src/pages/Login.vue (after the login POST)typescript
import { useRouter } from "vue-router";
import { useAuth } from "../stores/auth";
import { currentUser } from "../api";

const router = useRouter();
const auth = useAuth();

auth.user = await currentUser();
await router.push({ name: "posts" });

Gotcha: plain axios drops the cookie

currentUser() is GET /api/user on the credentialed client. A plain axios.get does not send the session cookie to another origin. Logging out clears auth.user and pushes { name: "login" } after Laravel’s logout endpoint returns.

Check the first paint

Log in, then hard-refresh on /. You should stay on the posts page. Open DevTools → Network: /api/user must finish before any redirect to /login.

A third party with its own client id is not this router. That decision is Sanctum or Passport. OAuth’s redirect lives on the other product’s server. Your first-party SPA keeps the cookie and these two routes.

Recap#

  • Vue Router is for a separate SPA; Inertia apps keep Laravel routes.
  • Guest and auth routes via meta and one beforeEach guard.
  • Load the user before the router runs; treat only 401 as logged out.
  • Use the credentialed client for every call.

FAQ#

Can I use Vue Router with Inertia?

No. Inertia already handles navigation from Laravel routes. A second router fights it.

Why does my Vue Router guard redirect to login after a refresh?

The guard runs before the user is loaded. Await GET /api/user before registering the router and mounting the app.

Why does refreshing a Vue Router page give a 404?

The SPA host must answer unknown paths with index.html, so the router can resolve them in the browser.

Next steps#

Sources#

Found this useful?

Ask questions, share what you built.

Join thousands of Laravel and Vue developers in our Facebook group, or get new posts in your Telegram feed. Sharing this article helps too.

Keep reading

Newsletter

Get the best of Laravel & Vue, once a month.

One short email with new guides, release notes worth reading and hand-picked community links. No spam, unsubscribe any time.