Skip to main content
Instead of generating a static configuration file, you can create an API endpoint that serves Ziggy’s configuration dynamically. This approach is particularly useful for SPAs or applications where the frontend is in a separate repository.

Overview

Serving Ziggy’s configuration from an API endpoint provides several benefits:
  • Always up-to-date: No need to regenerate static files when routes change
  • Dynamic filtering: Serve different routes based on authentication or user permissions
  • Simplified deployment: No need to copy config files between projects
  • Cache control: Implement custom caching strategies

Creating the Basic Endpoint

1

Add the API route

Create a route that returns a new Ziggy instance as JSON:
// routes/api.php

use Tighten\Ziggy\Ziggy;

Route::get('ziggy', fn () => response()->json(new Ziggy));
This endpoint will be available at /api/ziggy.
2

Test the endpoint

Visit the endpoint in your browser or use curl:
curl https://yourapp.com/api/ziggy
You should see a JSON response like:
{
    "url": "https://yourapp.com",
    "port": null,
    "defaults": {},
    "routes": {
        "home": {
            "uri": "/",
            "methods": ["GET", "HEAD"],
            "domain": null
        },
        "posts.index": {
            "uri": "posts",
            "methods": ["GET", "HEAD"],
            "domain": null
        }
    }
}
3

Fetch from your frontend

Fetch the configuration when your application initializes:
import { route } from 'ziggy-js';

let ziggyConfig = null;

async function loadZiggyConfig() {
    const response = await fetch('https://yourapp.com/api/ziggy');
    ziggyConfig = await response.json();
}

// Call this when your app starts
await loadZiggyConfig();

// Now you can use the route function
const url = route('posts.show', 1, undefined, ziggyConfig);

Advanced Configurations

Filtering Routes

Serve only specific routes to your frontend:
// routes/api.php

use Tighten\Ziggy\Ziggy;

Route::get('ziggy', function () {
    return response()->json(
        new Ziggy(only: ['api.*', 'posts.*'])
    );
});
Or exclude certain routes:
Route::get('ziggy', function () {
    return response()->json(
        new Ziggy(except: ['admin.*', '_debugbar.*'])
    );
});

Using Groups

Serve different route groups based on user permissions:
Route::get('ziggy', function () {
    $groups = ['public'];

    if (auth()->check()) {
        $groups[] = 'authenticated';
    }

    if (auth()->user()?->isAdmin()) {
        $groups[] = 'admin';
    }

    return response()->json(
        new Ziggy(group: $groups)
    );
});
Define groups in config/ziggy.php:
// config/ziggy.php

return [
    'groups' => [
        'public' => ['home', 'posts.index', 'posts.show'],
        'authenticated' => ['dashboard', 'profile.*'],
        'admin' => ['admin.*', 'users.*'],
    ],
];

Adding Cache Headers

Implement caching to reduce server load:
Route::get('ziggy', function () {
    $ziggy = new Ziggy;

    return response()
        ->json($ziggy)
        ->header('Cache-Control', 'public, max-age=3600') // Cache for 1 hour
        ->header('ETag', md5(json_encode($ziggy)));
});

Authenticated Endpoint

Protect the endpoint with authentication:
Route::middleware('auth:sanctum')->get('ziggy', function () {
    return response()->json(
        new Ziggy(only: ['api.*'])
    );
});

Frontend Integration

React Hook Example

Create a custom hook to fetch and manage Ziggy config:
import { useState, useEffect } from 'react';
import { route as ziggyRoute } from 'ziggy-js';

export function useZiggy() {
    const [config, setConfig] = useState(null);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState(null);

    useEffect(() => {
        fetch('/api/ziggy')
            .then(res => res.json())
            .then(data => {
                setConfig(data);
                setLoading(false);
            })
            .catch(err => {
                setError(err);
                setLoading(false);
            });
    }, []);

    const route = (name, params) => {
        if (!config) return '#';
        return ziggyRoute(name, params, undefined, config);
    };

    return { route, loading, error };
}
Use in your components:
import { useZiggy } from './hooks/useZiggy';

function PostsList() {
    const { route, loading } = useZiggy();

    if (loading) return <div>Loading...</div>;

    return (
        <div>
            <a href={route('posts.index')}>All Posts</a>
            <a href={route('posts.create')}>Create Post</a>
        </div>
    );
}

Vue Composable Example

// composables/useZiggy.js
import { ref, onMounted } from 'vue';
import { route as ziggyRoute } from 'ziggy-js';

const config = ref(null);
const loading = ref(true);

export function useZiggy() {
    onMounted(async () => {
        if (config.value) return; // Already loaded

        try {
            const response = await fetch('/api/ziggy');
            config.value = await response.json();
        } catch (error) {
            console.error('Failed to load Ziggy config:', error);
        } finally {
            loading.value = false;
        }
    });

    const route = (name, params) => {
        if (!config.value) return '#';
        return ziggyRoute(name, params, undefined, config.value);
    };

    return { route, loading };
}

Global Configuration Strategy

Load the config once and make it globally available:
// app.js
import { Ziggy } from 'ziggy-js';

async function initializeApp() {
    // Fetch Ziggy config
    const response = await fetch('/api/ziggy');
    const config = await response.json();

    // Make it globally available
    window.Ziggy = config;

    // Now initialize your app
    // ...
}

initializeApp();
Then use it anywhere without passing config:
import { route } from 'ziggy-js';

// Config is automatically picked up from window.Ziggy
const url = route('posts.show', 1);

Performance Optimization

Server-Side Caching

Cache the Ziggy config to avoid regenerating it on every request:
use Illuminate\Support\Facades\Cache;
use Tighten\Ziggy\Ziggy;

Route::get('ziggy', function () {
    $config = Cache::remember('ziggy-config', 3600, function () {
        return new Ziggy;
    });

    return response()->json($config);
});
Invalidate the cache when routes change:
// In your deployment script or AppServiceProvider
Cache::forget('ziggy-config');

Client-Side Caching

Cache the config in localStorage:
async function loadZiggyConfig() {
    // Try to load from cache
    const cached = localStorage.getItem('ziggy-config');
    const cacheTime = localStorage.getItem('ziggy-config-time');

    // Use cache if less than 1 hour old
    if (cached && cacheTime && Date.now() - parseInt(cacheTime) < 3600000) {
        return JSON.parse(cached);
    }

    // Fetch fresh config
    const response = await fetch('/api/ziggy');
    const config = await response.json();

    // Store in cache
    localStorage.setItem('ziggy-config', JSON.stringify(config));
    localStorage.setItem('ziggy-config-time', Date.now().toString());

    return config;
}

ETags for Cache Validation

Route::get('ziggy', function () {
    $ziggy = new Ziggy;
    $etag = md5(json_encode($ziggy));

    if (request()->header('If-None-Match') === $etag) {
        return response('', 304); // Not Modified
    }

    return response()
        ->json($ziggy)
        ->header('ETag', $etag)
        ->header('Cache-Control', 'must-revalidate');
});

Troubleshooting

Ensure the route is properly defined:
php artisan route:list --path=ziggy
Verify the route appears in the list. If not, clear the route cache:
php artisan route:clear
If your frontend is on a different domain, configure CORS:
// config/cors.php

return [
    'paths' => ['api/*'],
    'allowed_origins' => ['https://yourfrontend.com'],
    'allowed_methods' => ['GET'],
    'allowed_headers' => ['*'],
];
Check your route filtering configuration:
// Make sure filters aren't too restrictive
return response()->json(
    (new Ziggy)->toArray() // Debug: see all properties
);
If using Sanctum or other authentication:
const response = await fetch('/api/ziggy', {
    headers: {
        'Authorization': `Bearer ${token}`,
        'Accept': 'application/json',
    },
});

Security Considerations

  • Filter sensitive routes: Never expose admin or internal routes to public endpoints
  • Rate limiting: Apply rate limiting to prevent abuse
    Route::middleware('throttle:60,1')->get('ziggy', /* ... */);
    
  • Authentication: Consider requiring authentication for production apps
  • CORS: Only allow trusted origins in production

Best Practices

  1. Cache aggressively: Route configurations rarely change, so cache liberally
  2. Use groups: Filter routes appropriately for different user types
  3. Version your endpoint: Consider versioning (e.g., /api/v1/ziggy) for future changes
  4. Monitor performance: Track endpoint response times and cache hit rates
  5. Handle errors gracefully: Provide fallbacks if config loading fails

Build docs developers (and LLMs) love