Stop Hardcoding URLs — Use Ziggy route() in JavaScript
Every Laravel developer knows you should never hardcode URLs in PHP. That's what route('users.show', $user) is for. But then in JavaScript, the same developers write fetch('/api/users/' + userId) without blinking. Ziggy fixes this.
The problem
Hardcoded URLs in JavaScript are brittle. Rename a route, change a prefix, add a version — and you're grepping through .js and .vue files hoping you didn't miss one. There's no compiler to catch it, no IDE warning, just a 404 in production.
// Fragile — breaks silently when the route changes
fetch('/api/users/' + userId)
fetch('/api/v2/invoices?status=pending')
window.location = '/dashboard/settings'
What Ziggy does
Ziggy generates a JavaScript route() function that mirrors Laravel's. Name your routes in PHP, and they become available in JavaScript with full parameter support.
// Resilient — always matches the PHP route definition
route('api.users.show', { user: userId })
route('api.invoices.index', { status: 'pending' })
route('dashboard.settings')
Setup
Install the package:
composer require tightenco/ziggy
Add the @routes directive to your Blade layout, before your app scripts:
<head>
@routes
@vite(['resources/js/app.js'])
</head>
That's it. The route() function is now globally available in your JavaScript.
Name your routes
This only works if your routes have names. If you're using resource controllers, they're already named. For custom routes, add ->name():
// Resource routes are automatically named
Route::apiResource('users', UserController::class);
// users.index, users.store, users.show, users.update, users.destroy
// Custom routes need explicit names
Route::get('/dashboard/settings', [SettingsController::class, 'index'])
->name('dashboard.settings');
Parameters and query strings
Ziggy handles route parameters and query strings the same way Laravel does:
// Route model binding
route('users.show', { user: 42 })
// → /users/42
// Multiple parameters
route('teams.members.show', { team: 1, member: 5 })
// → /teams/1/members/5
// Extra parameters become query string
route('invoices.index', { status: 'pending', page: 2 })
// → /invoices?status=pending&page=2
Checking the current route
Ziggy also provides a route().current() method, which is useful for active navigation states:
// Check if we're on a specific route
route().current('dashboard.settings')
// → true/false
// Wildcard matching
route().current('dashboard.*')
// → true for any dashboard.* route
Filtering exposed routes
By default, Ziggy exposes all named routes to JavaScript. If you have admin routes you'd rather not leak to the client, filter them in config/ziggy.php:
// config/ziggy.php
return [
'only' => ['api.*', 'dashboard.*', 'public.*'],
// or exclude specific groups
'except' => ['admin.*', 'nova.*'],
];
Make it a rule
The real value comes from making this a team convention, not a suggestion. In my projects, hardcoded URLs in JavaScript are treated the same as hardcoded URLs in PHP — they don't pass review. Every route gets a name, every JavaScript fetch uses route(). No exceptions.
It's a small habit that eliminates an entire category of bugs. The next time you rename a route prefix or restructure your API, you'll change it in one place and everything just works.