Skip to main content

Overview

Ziggy supports Laravel’s route-model binding, including custom route key names. When you pass a JavaScript object as a route parameter, Ziggy automatically extracts the correct property value using the model’s binding configuration.

How It Works

When you pass an object as a route parameter, Ziggy:
  1. Checks if the route has a custom binding key defined
  2. If yes, uses that key to extract the value from the object
  3. If no custom key exists, falls back to the id property
const post = {
  id: 3,
  title: 'Introducing Ziggy v1',
  slug: 'introducing-ziggy-v1',
  date: '2020-10-23T20:59:24.359278Z',
};

route('posts.show', post);
// Ziggy uses the appropriate binding key automatically

Custom Route Keys

Laravel models can customize their route key using getRouteKeyName():
// app/Models/Post.php

class Post extends Model
{
    public function getRouteKeyName()
    {
        return 'slug';
    }
}
Route::get('blog/{post}', function (Post $post) {
    return view('posts.show', ['post' => $post]);
})->name('posts.show');
Ziggy automatically detects this configuration and uses the slug property:
const post = {
  id: 3,
  title: 'Introducing Ziggy v1',
  slug: 'introducing-ziggy-v1',
  date: '2020-10-23T20:59:24.359278Z',
};

route('posts.show', post);
// 'https://ziggy.test/blog/introducing-ziggy-v1'
// ✅ Uses 'slug' instead of 'id'
Ziggy reads the binding configuration from your Laravel routes when generating its JavaScript config. You don’t need to manually configure bindings in JavaScript.

Inline Custom Keys

Laravel allows you to specify custom binding keys directly in route definitions using the : syntax:
Route::get('authors/{author}/photos/{photo:uuid}', fn (Author $author, Photo $photo) => /* ... */)
    ->name('authors.photos.show');
Ziggy respects these inline bindings:
const photo = {
  uuid: '714b19e8-ac5e-4dab-99ba-34dc6fdd24a5',
  filename: 'sunset.jpg',
};

route('authors.photos.show', [{ id: 1, name: 'Ansel' }, photo]);
// 'https://ziggy.test/authors/1/photos/714b19e8-ac5e-4dab-99ba-34dc6fdd24a5'
// ✅ Uses 'uuid' for photo parameter

Scoped Bindings

Ziggy supports Laravel’s scoped bindings with custom keys:
Route::get('posts/{post}/comments/{comment:uuid}', fn (Post $post, Comment $comment) => /* ... */)
    ->name('posts.comments.show')
    ->scopeBindings();
const post = { id: 1, title: 'Hello World' };
const comment = {
  id: 42,
  uuid: '9b7f4e8a-8e1c-4e5a-9c1d-8e7b4a9c1d2e',
  body: 'Great post!',
};

route('posts.comments.show', [post, comment]);
// 'https://ziggy.test/posts/1/comments/9b7f4e8a-8e1c-4e5a-9c1d-8e7b4a9c1d2e'
While Ziggy uses the correct binding keys to generate URLs, the actual scope validation (ensuring the comment belongs to the post) happens on the Laravel backend.

How Bindings Are Resolved

Ziggy’s PHP code analyzes your routes to determine binding keys:
private function resolveBindings(array $routes): array
{
    foreach ($routes as $name => $route) {
        $bindings = [];

        foreach ($route->signatureParameters(UrlRoutable::class) as $parameter) {
            $model = Reflector::getParameterClassName($parameter);

            // Check if model overrides default route key name
            $override = (new ReflectionClass($model))->isInstantiable() && (
                (new ReflectionMethod($model, 'getRouteKeyName'))->class !== Model::class
                || (new ReflectionMethod($model, 'getKeyName'))->class !== Model::class
                || (new ReflectionProperty($model, 'primaryKey'))->class !== Model::class
            );

            // Use custom key or default to 'id'
            $bindings[$parameter->getName()] = $override ? app($model)->getRouteKeyName() : 'id';
        }

        // Merge with inline binding fields (e.g., {photo:uuid})
        $routes[$name] = [...$bindings, ...$route->bindingFields()];
    }

    return $routes;
}
This method:
  1. Inspects route parameters that implement UrlRoutable
  2. Checks if the model customizes its route key
  3. Stores the binding configuration in Ziggy’s config
  4. Merges inline binding fields from the route definition

Binding Substitution in JavaScript

When you pass an object, Ziggy’s JavaScript code extracts the value:
_substituteBindings(params, { bindings, parameterSegments }) {
    return Object.entries(params).reduce((result, [key, value]) => {
        // Skip if not an object or not a route parameter
        if (
            !value ||
            typeof value !== 'object' ||
            Array.isArray(value) ||
            !parameterSegments.some(({ name }) => name === key)
        ) {
            return { ...result, [key]: value };
        }

        // Find the binding key
        const binding = value.hasOwnProperty(bindings[key])
            ? bindings[key]
            : value.hasOwnProperty('id')
              ? 'id'
              : undefined;

        if (binding === undefined) {
            throw new Error(
                `Ziggy error: object passed as '${key}' parameter is missing route model binding key '${bindings[key]}'.`
            );
        }

        return { ...result, [key]: value[binding] };
    }, {});
}

Error Handling

If you pass an object that doesn’t have the required binding key, Ziggy throws an error:
const post = { title: 'Hello' }; // Missing 'slug' key

route('posts.show', post);
// Error: Ziggy error: object passed as 'post' parameter is missing route model binding key 'slug'.

Fallback to ID

If no custom binding key is defined, Ziggy defaults to using the id property:
Route::get('users/{user}', fn (User $user) => /* ... */)->name('users.show');
// User model doesn't override getRouteKeyName()
const user = { id: 5, name: 'Jane Doe' };

route('users.show', user);
// 'https://ziggy.test/users/5'
// ✅ Falls back to 'id'

Mixed Parameter Types

You can mix objects with route-model binding and primitive values:
const author = {
  id: 1,
  name: 'Ansel Adams',
  slug: 'ansel-adams',
};

// Object + primitive
route('authors.photos.show', [author, '714b19e8']);
// 'https://ziggy.test/authors/ansel-adams/photos/714b19e8'

// Object + object
const photo = { uuid: '714b19e8', filename: 'sunset.jpg' };
route('authors.photos.show', [author, photo]);
// 'https://ziggy.test/authors/ansel-adams/photos/714b19e8'

Best Practices

// ✅ Let Ziggy handle binding resolution
route('posts.show', post);

// ❌ Manual property access defeats the purpose
route('posts.show', post.slug);
// ✅ Object has all binding keys
const post = await axios.get('/api/posts/1');
route('posts.show', post.data); // Has 'slug' property

// ❌ Incomplete object will cause errors
const partial = { title: post.title };
route('posts.show', partial); // Missing 'slug'!
If you change a model’s getRouteKeyName() method, regenerate Ziggy’s config:
php artisan ziggy:generate
Or refresh the page if using the @routes Blade directive.

Common Use Cases

API Responses

When working with API data that includes model objects:
axios.get('/api/posts').then((response) => {
  response.data.forEach((post) => {
    const url = route('posts.show', post);
    console.log(`${post.title}: ${url}`);
  });
});

Vue Components

<template>
  <a :href="route('posts.show', post)">
    {{ post.title }}
  </a>
</template>

<script setup>
const post = {
  id: 1,
  slug: 'hello-world',
  title: 'Hello World',
};
</script>

React Components

import { useRoute } from 'ziggy-js';

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

  return (
    <a href={route('posts.show', post)}>
      {post.title}
    </a>
  );
}

Next Steps

Default Parameters

Learn about default parameter values

TypeScript Support

Get autocomplete for routes and parameters

Build docs developers (and LLMs) love