Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Vue Spa Laravel Package

cretueusebiu/laravel-vue-spa

Laravel Vue SPA is a starter kit for building single-page applications with Laravel and Vue.js. It includes authentication, API scaffolding, routing, and a ready-to-use frontend/backend setup to help you launch modern SPAs quickly.

View on GitHub
Deep Wiki
Context7

Getting Started

Bring it up. The package is a starter kit, not a library you composer require into an existing app — you clone/copy it as the project root.

composer install
cp .env.example .env && php artisan key:generate
# set DB_* and APP_URL in .env
php artisan migrate --seed
npm install
npm run dev       # or `npm run watch` while developing

The seed step matters: authentication and the demo pages expect at least one user row, and the SPA's login page will otherwise appear broken on first run.

Where to look first, in this order.

  1. routes/web.php — there is a catch-all (/ {any}) that serves the single Blade shell. Everything that is not /api/* lands here. Understand this before anything else, because it is the source of most "my route returns HTML" confusion.
  2. routes/api.php — the actual backend surface the Vue app talks to. Every action your SPA performs is really a request to one of these.
  3. resources/js/router.js — the frontend route table. Page components are imported here and mapped to layouts; this file is the join between URL and component.
  4. resources/js/app.js — the bootstrap: Vue root instance, store injection, the global axios setup, and the router.beforeResolve hook that drives asyncData.
  5. resources/js/store/ — Vuex root plus modules (auth is the one you must read). State shape here dictates how components read user/session data.
  6. resources/js/pages/ and resources/js/layouts/ — home.vue in 6.0.1 is the reference implementation for asyncData; the layouts are where header/nav/auth-conditional chrome lives.

First use case to run end-to-end. Add one authenticated page backed by one API endpoint — nothing more:

  1. php artisan make:controller Api/ProjectsController and add Route::get('/projects', ...)->middleware('auth:api') to routes/api.php.
  2. Create resources/js/pages/projects/index.vue.
  3. Register it in resources/js/router.js under a route with meta: { requiresAuth: true }.
  4. Fetch the data in the component's asyncData (see below) and render it.

Doing this once touches every subsystem — auth guard, axios/token, store, layout, build — and will surface any environment problems immediately. Resist the urge to start with a complex feature.


Implementation Patterns

Adding a page

The unit of work is always the same three-file transaction: page component, router entry, nav link. Skipping the router entry is the single most common reason a page "doesn't exist." Route definitions carry their own metadata (requiresAuth, layout, and any title/breadcrumb you standardize), so the router file doubles as the app's route policy document — keep it tidy and treat it as reviewable config.

// resources/js/router.js
{
  path: '/projects',
  name: 'projects.index',
  component: () => import('@/pages/projects/index.vue'),
  meta: { requiresAuth: true, layout: 'default' }
}

Lazy import() per route keeps the initial bundle small; do this for everything past the landing page.

Data fetching: asyncData vs lifecycle hooks

asyncData (added in 6.0.1, reference implementation in resources/js/pages/home.vue) is the route-level prefetch hook — the router's beforeResolve calls it before the component renders, so the page mounts with data already in the store rather than flashing empty. Use it for the data a page cannot usefully render without.

export default {
  name: 'projects.index',

  asyncData({ store, route }) {
    // resolved before navigation completes
    return store.dispatch('projects/fetchIndex', route.query.page)
  },

  computed: {
    projects() { return this.$store.state.projects.items }
  }
}

Use created/mounted instead for anything that is (a) user-triggered, (b) safe to load after paint (secondary lists, analytics, recommendations), or (c) expensive enough that blocking navigation is a bad trade. The rule of thumb: block on the primary content, defer everything else. Blocking every fetch makes the app feel slower than not having asyncData at all.

For pagination/filtering, put the query in the route (?page=2) and read it from route inside asyncData — that way back/forward and deep links work, and you avoid a second source of truth for "what page am I on."

State: one Vuex module per domain

Mimic the existing auth module: state, mutations, actions, getters in resources/js/store/modules/<domain>.js, registered in store/index.js. Keep HTTP calls in actions, not components, so asyncData and component methods share one code path. For CRUD, standardize on fetchIndex / fetchSingle / store / update / destroy — the consistency pays off when the next developer joins.

Persist only what must survive a reload (the auth token). Everything else should be re-derivable from an API call; a stale Vuex cache is much worse than a refetch.

Auth and API calls

The axios instance is configured centrally in app.js (base URL, token header, response interceptor). Two patterns to lean on:

  • Request interceptor: attach the bearer token from the store/localStorage on every request. Never read the token inside a component.
  • Response interceptor: on 401, commit the logout mutation and router.push({ name: 'login' }). Do this in exactly one place — scattered 401 handling produces redirect loops and half-logged-out states.

For authenticated navigation, rely on the router guard reading meta.requiresAuth plus store.getters['auth/check'], and remember it is a UX affordance only. Every protected API route must independently carry auth:api middleware — the guard is trivially bypassed by anyone with curl.

Forms and validation

Use the shipped Form helper class (resources/js/libs/Form.js in this codebase) rather than hand-rolling error bags. It gives you form.post(url), form.errors.get('email'), form.busy, and automatic unwrapping of Laravel's 422 { errors: {...} } payload. That means your controller validation failures render into has-error classes with no extra glue. Bind inputs with v-model="form.email" and disable the submit button on form.busy — without that you will get duplicate registrations and duplicate orders.

Layout as a composition boundary

Layouts (resources/js/layouts/) own the shell: nav, footer, flash messages, auth-conditional chrome. Pages own content. Resist putting nav logic in pages; when a second layout appears (auth screens typically use a bare/basic layout), you want the switch to be a single meta.layout value rather than a conditional graph inside components.

Integration tips

  • Keep all API URLs relative (/api/...) so APP_URL drives both dev and prod. Hard-coding localhost:8000 is the most common cause of "works on my machine."
  • Use Laravel resources/transformers for response shape. The moment the SPA reads raw Eloquent models, every column rename becomes a frontend breaking change.
  • For anything real-time, add Echo/Pusher on top of the existing bootstrap rather than bolting on a second websocket client — the Vue prototype already has the store and the auth token to authenticate a private channel.
  • Standardize an asyncData contract early: it should return its promise (the router hook awaits the return value), read everything from { store, route }, and never touch this.

Gotchas and Tips

asyncData is not server-side rendering. This is the single most important thing to internalize. The Blade shell is still empty HTML served by PHP; asyncData resolves data before the client-side route component renders. That eliminates the blank-page flash within the SPA, but it does nothing for crawlability or true first contentful paint. If a stakeholder is told "pages now render with data on first load," clarify: it means "no spinner between routes," not "indexable HTML." Anything SEO-critical needs a prerender step, an SSR server, or server-rendered Blade for that specific page.

Unhandled rejections in asyncData block or break navigation. If the dispatch throws, the router's beforeResolve can leave you on a blank screen or stuck on the previous route. Always give the action a try/catch that commits an error state and returns a resolved value, and render an error UI in the page. Also add a timeout — a slow upstream API will freeze navigation entirely, which feels far worse than a spinner.

Rapid navigation races. Click through three menu items quickly and three asyncData calls are in flight; the last one to resolve wins, not the last one clicked. Guard with the route object you were given (compare against this.$route / router.currentRoute) or use a per-navigation request id. Symptom to watch for: data from page A briefly appearing on page B.

Logout does not clear your modules. The auth module clears its own state; your custom modules keep whatever the previous user loaded. Register a root-level RESET_STATE mutation that every module handles, or reload the app on logout. This is a real data-leak bug in multi-user-per-device deployments.

Token storage. Whatever the starter does (localStorage or cookie), be consistent. localStorage is XSS-readable; cookie-based needs CSRF considerations and withCredentials. Do not mix — a token in both places with divergent lifetimes produces intermittent 401s that are extremely hard to reproduce.

The catch-all route swallows everything. Any new server-side route must be registered before the SPA fallback, and must not collide with /api. A new Route::get('/pricing', ...) added after the catch-all will silently return the Vue shell, and you will spend an hour wondering why your Blade view never renders.

MIX_ prefix or it doesn't exist. Frontend-visible env vars need MIX_ in .env, and changing .env requires a rebuild (npm run dev restart or a fresh npm run production). "My env var is undefined" is almost always a stale bundle.

This package is pinned to an aging stack. Last release was August 2021 — expect Vue 2, an older Laravel line, and Laravel Mix rather than Vite. Pin exact versions in both composer.json and package.json; do not let ^ ranges float a feature-bearing patch into a routine dependency PR. Before upgrading, diff the vendor tree rather than trusting the one-line release note, and check your Node version against the build toolchain — a Node major bump will break Mix builds in ways that look like application bugs.

Published assets drift. If you've customized anything under resources/js that also ships in the package, upgrading means re-diffing, not overwriting. Keep a short runbook: pull the new tag into a scratch directory, diff against your resources/js and routes/, merge by hand. Teams that vendor:publish --force lose their customizations and don't notice until regression.

Debugging checklist, in order: network tab (is it hitting /api or getting HTML back?), then Vuex devtools (did the action commit?), then the router (beforeResolve guard order — a global guard added after the asyncData hook will short-circuit it), then the build (stale chunk / HMR desync; restart npm run watch).

Extension points worth knowing. The axios instance in app.js is the right place for global headers, retry logic, and error normalization. The router's beforeEach/beforeResolve chain is where you add page-title setting, analytics pageviews, and permission checks. The Form helper is where you add file uploads (multipart) or custom error mapping — extend it once rather than overriding per-component.

Ramp-up expectation. A Laravel developer can be productive on the backend half in a day, but the frontend half — Vuex module conventions, router guard ordering, asyncData semantics, Mix build behavior — is roughly one to two engineer-weeks to independent productivity. Budget for it, and designate one owner for the JS build pipeline early.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle