Using Laravel Named Routes in JavaScript with Ziggy

Using Laravel Named Routes in JavaScript with Ziggy

If you’ve been working with Laravel named routes in JavaScript, you already know the pain. You write a beautiful, clean Laravel backend — named routes everywhere, route('posts.show', $post->id) in your Blade templates — and then you jump to your JavaScript file and suddenly everything falls apart. You’re typing /api/posts/1 by hand. Again.

And when that route changes? You go hunting through every .js file hoping you haven’t missed one.

There’s a better way. It’s called Ziggy, and once you start using it, you’ll wonder how you ever shipped without it.

What Is Laravel Named Routes in JavaScript — And Why You Keep Getting It Wrong

Let’s be honest for a second. Most of us start a Laravel + JavaScript project with the best intentions. We name our routes properly in routes/web.php, we use route() in Blade like responsible adults, and then… we write JavaScript like it’s 2012.

Here’s what the typical JS file looks like on a real project:

PHP
// 😬 Please don't do this
axios.get('/api/posts/' + postId)
axios.post('/api/comments/store')
axios.delete('/users/' + userId + '/settings')

This works. Until it doesn’t.

The moment someone moves a route into a prefixed group, or renames a parameter, or adds a version prefix to the API — your JavaScript breaks silently. No error from Laravel. No warning. Just a 404 that you’ll spend an hour debugging.

The reason we hardcode URLs in JavaScript is simple: we don’t have a way to access Laravel’s route definitions from the JS side. The route() helper is a PHP function. It lives on the server. Your JavaScript doesn’t know it exists.

That’s the problem Ziggy was built to solve.

Meet Ziggy — The Bridge Between Laravel Routes and Your JavaScript

Ziggy is an open-source package by Tighten — the same team behind tools like Pint, Takeout, and Lambo. The package was created because Tighten’s own developers got tired of hardcoded URLs in their client JavaScript.

Here’s the core idea:

Ziggy exposes all your Laravel named routes to JavaScript, and gives you a route() helper function that works exactly like the PHP one.

That means this Blade code:

PHP
{{ route('posts.show', $post->id) }}

Can now become this in your JavaScript:

PHP
route('posts.show', post.id)
// → 'https://yourapp.com/posts/42'

Same API. Same mental model. No hardcoded strings.

Ziggy works by injecting a small JavaScript object into your HTML (via a Blade directive) that contains all your route definitions. Then the route() function uses that object to build URLs dynamically — on the client side, without any server round-trips.

It’s popular with teams using Laravel as a backend and Vue, React, or vanilla JS on the frontend. It also works seamlessly with Inertia.js.

Installing Ziggy — Step by Step

Let’s get your project set up. This assumes you have a standard Laravel app running.

Step 1: Install via Composer

PHP
composer require tightenco/ziggy

That’s the backend side sorted. Laravel will auto-discover the service provider, so you don’t need to manually register anything in config/app.php on modern Laravel versions.

Step 2: Add the @routes Directive to Your Blade Layout

Open your main layout file — usually resources/views/layouts/app.blade.php — and add the @routes directive before your app’s JavaScript:

PHP
<!DOCTYPE html>
<html>
<head>
    <title>My App</title>
</head>
<body>
    <!-- Your page content -->
    @yield('content')

    {{-- Add this BEFORE your app JS --}}
    @routes

    @vite(['resources/js/app.js'])
</body>
</html>

This directive outputs a <script> tag that injects all your named routes as a JavaScript object. It’s what makes route() work in the browser.

Step 3: Use Ziggy’s route() in Your JavaScript

That’s literally it. Once you’ve done those two steps, the route() helper is available globally in your JavaScript files. No imports needed for basic Blade-based setups.

PHP
// resources/js/app.js or any JS file
console.log(route('home'))         // → 'https://yourapp.com/'
console.log(route('posts.index'))  // → 'https://yourapp.com/posts'

How route() Works — The Basics

The route() function in Ziggy mirrors Laravel’s PHP helper almost perfectly. Here’s what you need to know.

Simple Routes (No Parameters)

PHP
// PHP equivalent: route('home')
route('home')
// → 'https://yourapp.com/'

// PHP equivalent: route('posts.index')
route('posts.index')
// → 'https://yourapp.com/posts'

Routes with Parameters

Pass parameters as the second argument. You can pass a single value, an array, or an object — just like in PHP.

PHP
// Your Laravel route:
// Route::get('/posts/{post}', ...)->name('posts.show');

route('posts.show', 1)
// → 'https://yourapp.com/posts/1'

route('posts.show', [1])
// → 'https://yourapp.com/posts/1'

route('posts.show', { post: 1 })
// → 'https://yourapp.com/posts/1'

Multiple Parameters

PHP
// Route::get('/events/{event}/venues/{venue}', ...)->name('events.venues.show');

route('events.venues.show', [1, 2])
// → 'https://yourapp.com/events/1/venues/2'

route('events.venues.show', { event: 1, venue: 2 })
// → 'https://yourapp.com/events/1/venues/2'

Query String Parameters

If you pass extra keys that aren’t in the route definition, Ziggy automatically appends them as query strings:

PHP
route('posts.index', { page: 2, sort: 'date' })
// → 'https://yourapp.com/posts?page=2&sort=date'

This is really useful for filtering and pagination — things you’d normally construct with string concatenation.

Checking the Current Route

One thing that doesn’t exist in PHP’s route helper (because it doesn’t need to) is the ability to check what route you’re currently on. Ziggy adds this as a bonus.

PHP
// Are we on the posts.index route?
route().current('posts.index')
// → true or false

// What route are we currently on?
route().current()
// → 'posts.index'

This is great for active nav link highlighting without server-side logic:

PHP
// Highlight nav links in vanilla JS
if (route().current('dashboard')) {
  document.querySelector('#nav-dashboard').classList.add('active')
}

Route Model Binding — Ziggy Gets It

Here’s something that surprises most developers: Ziggy supports route model binding. If you pass a JavaScript object as a route parameter, Ziggy is smart enough to extract the right key from it.

PHP
// Your Laravel route uses route model binding:
// Route::get('/posts/{post}', ...)->name('posts.show');

const post = { id: 42, title: 'Hello World', slug: 'hello-world' }

route('posts.show', post)
// → 'https://yourapp.com/posts/42'
// Ziggy automatically uses the 'id' key

If you’ve set a custom route key (via getRouteKeyName() in your model), Ziggy respects that too.

Real-World Example — Form Submission and Redirect in JavaScript

Let’s put everything together with a practical example. Imagine you have a form to create a new post, and after submission you want to redirect the user to the post’s show page.

Your Laravel routes:

PHP
// routes/web.php
Route::get('/posts', [PostController::class, 'index'])->name('posts.index');
Route::post('/posts', [PostController::class, 'store'])->name('posts.store');
Route::get('/posts/{post}', [PostController::class, 'show'])->name('posts.show');

JavaScript (using Axios):

PHP
// resources/js/components/CreatePost.js

async function submitPost(formData) {
  try {
    // ✅ No hardcoded URL — using named route
    const response = await axios.post(route('posts.store'), formData)

    const newPost = response.data

    // ✅ Redirect to the new post using named route with ID
    window.location.href = route('posts.show', newPost.id)

  } catch (error) {
    console.error('Something went wrong:', error.response.data)
  }
}

Vue component example:

PHP
<template>
  <form @submit.prevent="handleSubmit">
    <input v-model="form.title" placeholder="Post title" />
    <textarea v-model="form.body" placeholder="Write something..."></textarea>
    <button type="submit">Publish</button>
  </form>
</template>

<script>
import axios from 'axios'

export default {
  data() {
    return {
      form: {
        title: '',
        body: ''
      }
    }
  },
  methods: {
    async handleSubmit() {
      // ✅ Clean, named-route-based POST
      const response = await axios.post(route('posts.store'), this.form)

      // ✅ Redirect to the new post
      window.location.href = route('posts.show', response.data.id)
    }
  }
}
</script>

No hardcoded URLs. No string concatenation. If you ever rename the route or change the URL structure, your JS code doesn’t need to change at all.

Using Ziggy with Vue and React

Ziggy ships with first-class support for both Vue and React.

Vue

Register the Ziggy plugin in your app.js:

PHP
import { createApp } from 'vue'
import { ZiggyVue } from '../../vendor/tightenco/ziggy'
import App from './App.vue'

createApp(App)
  .use(ZiggyVue)
  .mount('#app')

Now route() is available in every Vue component without importing anything:

PHP
<template>
  <a :href="route('posts.index')">All Posts</a>
</template>

React

Use the useRoute hook:

PHP
import { useRoute } from 'ziggy-js'

export default function PostLink({ postId }) {
  const route = useRoute()

  return (
    <a href={route('posts.show', postId)}>
      View Post
    </a>
  )
}

With Inertia.js

If you’re using Inertia.js, Ziggy is basically essential. The combo is extremely popular. You get server-side rendering benefits while keeping all your route references clean and centralized.

Filtering Routes (Keep It Secure)

By default, Ziggy exposes all your named routes to the frontend. That’s usually fine for full-stack apps, but you might not want admin routes visible in the HTML source of public pages.

You can filter routes in config/ziggy.php (publish the config first with php artisan vendor:publish):

PHP
// config/ziggy.php
return [
    'only' => ['home', 'posts.*', 'profile.*'],
    // OR
    'except' => ['admin.*', 'horizon.*'],
];

You can also pass groups directly to the directive on specific pages:

PHP
@routes(['group' => 'public'])

Important note from the docs: Hiding routes from Ziggy’s output is not a replacement for proper authentication and authorization. Always protect sensitive routes with middleware on the server side.

Non-Blade Setup — Generating a Routes File

If you’re not using Blade (maybe you’re building a fully decoupled SPA), Ziggy has you covered. Run:

PHP
php artisan ziggy:generate

This creates resources/js/ziggy.js with all your routes exported as a JavaScript module. You can then import it in your frontend project and pass it to the route() function manually.

You can also expose your Ziggy config as a JSON API endpoint:

PHP
// routes/api.php
use Tighten\Ziggy\Ziggy;

Route::get('/ziggy', fn () => response()->json(new Ziggy));

This is useful for decoupled frontends that fetch config on load.

TypeScript Support

Ziggy also comes with TypeScript support. Run php artisan ziggy:generate --types to generate a TypeScript definition file. This gives you autocomplete for route names and parameters in your editor — which is an absolute game-changer when working on large Laravel apps.

PHP
// You'll get autocomplete for route names
route('posts.show', { post: 1 }) // ✅ Typed!
route('posts.typo', 1)           // ❌ TypeScript will catch this

Why Ziggy Over Just Using Axios With Hardcoded URLs?

Let me break it down plainly:

Hardcoded URLs:

  • Break silently when routes change
  • Scatter business logic across JS files
  • Are a nightmare to refactor
  • Require find-and-replace during route changes

Ziggy with named routes:

  • Update automatically when routes change (just re-run ziggy:generate if needed)
  • Keep your frontend code in sync with your backend
  • Match the mental model you already have from Blade
  • Work with route parameters, query strings, and model binding

The maintenance cost alone makes Ziggy worth it on any project bigger than a personal todo app.

Quick Reference Cheatsheet

PHP
// Basic route
route('home')
// → '/​'

// Route with single param
route('posts.show', 1)
// → '/posts/1'

// Route with named param
route('posts.show', { post: 1 })
// → '/posts/1'

// Route with multiple params
route('events.venues.show', { event: 1, venue: 2 })
// → '/events/1/venues/2'

// Route with query string
route('posts.index', { page: 2, search: 'laravel' })
// → '/posts?page=2&search=laravel'

// Check current route
route().current('posts.*')
// → true if on any posts route

// Get current route name
route().current()
// → 'posts.index'

Wrapping Up

Ziggy is one of those packages that feels obvious the moment you start using it. It fills a gap that almost every Laravel + JavaScript developer has hit: you want the safety and maintainability of named routes, but your JavaScript doesn’t know what Laravel routes even are.

With Ziggy, that gap disappears. Your route() helper works the same in PHP and in JavaScript. Route changes propagate automatically. Your codebase stays clean.

If you’re building anything with Laravel as a backend and JavaScript on the frontend — whether that’s Vue, React, Inertia, or even plain Axios calls — Ziggy deserves a spot in your project.

Check out the official repo here: https://github.com/tighten/ziggy

And the docs are at: https://tighten-ziggy.mintlify.app


Found this useful? Share it with your team. Beginners especially will thank you — this is one of those things that nobody teaches you but everyone eventually needs.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *