Routing
Saucebase uses Laravel's routing system with Inertia.js for seamless SPA navigation. This guide covers how routing works in the modular architecture and how to use Ziggy for client-side routing.
Overview
Saucebase routing combines three key components:
- Laravel Routes - Define server-side endpoints
- Inertia.js - Handle SPA navigation without page reloads
- Ziggy - Access Laravel routes from JavaScript/TypeScript
Defining Routes
Core Application Routes
Core routes are defined in routes/web.php:
// routes/web.php
use Illuminate\Support\Facades\Route;
use Inertia\Inertia;
Route::get('/', function () {
return Inertia::render('Index')->withSSR();
});
Route::get('/dashboard', function () {
return Inertia::render('Dashboard');
})->middleware('auth');
Module Routes
Modules define their own routes in modules/<ModuleName>/routes/web.php:
// modules/Auth/routes/web.php
use Illuminate\Support\Facades\Route;
use Modules\Auth\app\Http\Controllers\AuthController;
Route::prefix('auth')->name('auth.')->group(function () {
Route::get('/login', [AuthController::class, 'showLogin'])->name('login');
Route::post('/login', [AuthController::class, 'login']);
Route::post('/logout', [AuthController::class, 'logout'])->name('logout');
});
Module routes are automatically loaded when the module is enabled.
Inertia Page Resolution
Saucebase extends Inertia's page resolution to support modular architecture with namespace syntax.
Core Pages
Render pages from resources/js/pages/:
// Renders: resources/js/pages/Dashboard.vue
return Inertia::render('Dashboard');
// Renders: resources/js/pages/Settings/Profile.vue
return Inertia::render('Settings/Profile');
Module Pages
Use namespace syntax to render module pages from modules/<ModuleName>/resources/js/pages/:
// Renders: modules/Auth/resources/js/pages/Login.vue
return Inertia::render('Auth::Login');
// Renders: modules/Settings/resources/js/pages/Index.vue
return Inertia::render('Settings::Index');
// Renders: modules/Settings/resources/js/pages/Profile/Edit.vue
return Inertia::render('Settings::Profile/Edit');
Namespace format: ModuleName::PagePath
How It Works
The resolveModularPageComponent() function in resources/js/lib/utils.ts handles page resolution:
- Checks if the page name contains
:: - If yes, extracts module name and page path
- Resolves to:
modules/<Module>/resources/js/pages/<Page>.vue - If no, resolves to:
resources/js/pages/<Page>.vue
Route Groups and Middleware
Grouping Routes
Use Laravel's route groups to organize routes:
// Authenticated routes
Route::middleware(['auth'])->group(function () {
Route::get('/dashboard', fn() => Inertia::render('Dashboard'));
Route::get('/profile', fn() => Inertia::render('Profile'));
});
// Admin routes
Route::middleware(['auth', 'role:admin'])->prefix('admin')->group(function () {
Route::get('/users', fn() => Inertia::render('Admin/Users'));
});
Common Middleware
Saucebase includes these middleware by default:
auth- Require authenticationguest- Require guest (not authenticated)role:admin- Require admin role (Spatie Permission)permission:edit-posts- Require specific permission
Client-Side Routing with Ziggy
Ziggy makes Laravel routes available in JavaScript/TypeScript.
Installation
Ziggy is already configured in Saucebase. Routes are available via the global route() function.
Basic Usage
// Generate URLs
route('dashboard'); // /dashboard
route('settings.profile'); // /settings/profile
// Routes with parameters
route('user.show', { id: 1 }); // /users/1
route('post.show', { post: 42 }); // /posts/42
// Multiple parameters
route('post.comments.show', { post: 1, comment: 5 }); // /posts/1/comments/5
Query Strings
Add query parameters using the _query option:
route('search', {
_query: {
q: 'laravel',
page: 2,
},
}); // /search?q=laravel&page=2
Route Checking
// Check if route exists
route().has('dashboard'); // true/false
// Check current route
route().current(); // 'dashboard'
route().current('dashboard'); // true/false
// Wildcard matching
route().current('settings.*'); // true if on any settings.* route
Navigation with Inertia
Combine Ziggy with Inertia's router for SPA navigation:
<script setup lang="ts">
import { router } from '@inertiajs/vue3';
// Navigate to a route
const goToDashboard = () => {
router.visit(route('dashboard'));
};
// Navigate with method
const deletePost = (id: number) => {
router.delete(route('post.destroy', { post: id }));
};
// Navigate with data
const updateProfile = (data: ProfileData) => {
router.put(route('profile.update'), data);
};
</script>
<template>
<Link :href="route('dashboard')">Dashboard</Link>
<button @click="goToDashboard">Go to Dashboard</button>
</template>
Route Model Binding
Laravel's route model binding works seamlessly with Inertia:
use App\Models\Post;
// Automatic binding by ID
Route::get('/posts/{post}', function (Post $post) {
return Inertia::render('Posts/Show', [
'post' => $post,
]);
});
// Custom binding (by slug)
Route::get('/posts/{post:slug}', function (Post $post) {
return Inertia::render('Posts/Show', [
'post' => $post,
]);
});
Client-side usage:
// Works with both ID and custom bindings
route('post.show', { post: 1 }); // /posts/1
route('post.show', { post: 'my-first-post' }); // /posts/my-first-post
Named Routes
Always use named routes for maintainability:
// ✅ Good: Named route
Route::get('/dashboard', fn() => Inertia::render('Dashboard'))
->name('dashboard');
// ✅ Good: Route group names
Route::prefix('settings')->name('settings.')->group(function () {
Route::get('/', fn() => Inertia::render('Settings/Index'))->name('index');
Route::get('/profile', fn() => Inertia::render('Settings/Profile'))->name('profile');
});
// Creates: settings.index, settings.profile
// ❌ Bad: Unnamed route
Route::get('/dashboard', fn() => Inertia::render('Dashboard'));
Locale Routing
Saucebase supports multi-language routing with locale prefixes:
// routes/web.php
Route::get('/locale/{locale}', function ($locale) {
if (in_array($locale, ['en', 'pt_BR'])) {
session(['locale' => $locale]);
}
return redirect()->back();
})->name('locale');
Client-side usage:
<script setup lang="ts">
import { router } from '@inertiajs/vue3';
const changeLocale = (locale: string) => {
router.visit(route('locale', { locale }));
};
</script>
<template>
<button @click="changeLocale('en')">English</button>
<button @click="changeLocale('pt_BR')">Português</button>
</template>
API Routes
API routes are defined in routes/api.php and module routes/api.php:
// routes/api.php
use Illuminate\Support\Facades\Route;
Route::middleware('auth:sanctum')->group(function () {
Route::get('/user', function (Request $request) {
return $request->user();
});
});
API routes are automatically prefixed with /api and use stateless authentication.
Route Caching
In production, cache routes for better performance:
# Cache routes
php artisan route:cache
# Clear route cache
php artisan route:clear
# List all routes
php artisan route:list
Route caching doesn't work with closures. Always use controller methods in production routes.
// ✅ Works with caching
Route::get('/dashboard', [DashboardController::class, 'index']);
// ❌ Doesn't work with caching
Route::get('/dashboard', fn() => Inertia::render('Dashboard'));
Security Best Practices
CSRF Protection
All POST, PUT, PATCH, DELETE routes are protected by CSRF middleware by default:
<script setup lang="ts">
import { useForm } from '@inertiajs/vue3';
const form = useForm({
name: '',
email: '',
});
const submit = () => {
// CSRF token automatically included
form.post(route('profile.update'));
};
</script>
Route Filtering
Consider filtering routes exposed to the frontend in config/ziggy.php:
return [
'except' => ['admin.*', 'sanctum.*', '_ignition.*'],
];
This reduces bundle size and hides internal routes (though it's not a security mechanism).
Authorization
Always implement proper authorization:
// Using middleware
Route::get('/posts/{post}/edit', [PostController::class, 'edit'])
->middleware('can:update,post');
// Using gates in controller
public function edit(Post $post)
{
$this->authorize('update', $post);
return Inertia::render('Posts/Edit', ['post' => $post]);
}
Common Patterns
Redirects After Actions
public function store(Request $request)
{
$post = Post::create($request->validated());
return redirect()->route('post.show', $post)
->with('success', 'Post created successfully');
}
Back Button Navigation
return redirect()->back()->with('error', 'Something went wrong');
Flash Messages
Flash messages persist through redirects:
return redirect()->route('dashboard')
->with('success', 'Profile updated!')
->with('error', 'Email already taken');
Access in Vue:
<script setup lang="ts">
import { usePage } from '@inertiajs/vue3';
const page = usePage();
const flash = computed(() => page.props.flash);
</script>
<template>
<div v-if="flash.success" class="alert-success">
{{ flash.success }}
</div>
</template>
Troubleshooting
Routes Not Found (404)
- Clear route cache:
php artisan route:clear - Check module is enabled: Verify
modules_statuses.json - List all routes:
php artisan route:list - Check middleware: Ensure you're authenticated if route requires auth
Ziggy Routes Not Available
- Rebuild assets:
npm run buildor restartnpm run dev - Check Ziggy config:
config/ziggy.php - Verify routes are named: Unnamed routes aren't available in Ziggy
Module Routes Not Loading
- Enable module:
php artisan module:enable <ModuleName> - Clear cache:
php artisan optimize:clear - Check routes file exists:
modules/<ModuleName>/routes/web.php
Inertia Page Not Found
- Check page path: Verify file exists at resolved path
- Rebuild assets: Module pages need
npm run buildafter changes - Check namespace syntax: Use
Module::Pagefor module pages
Next Steps
- Translations - Learn about multi-language support
- SSR - Understand server-side rendering
- Modules - Learn about the Auth module for authentication