Introducing Ziggy - a package providing Laravel Named Routes in JavaScript

Feature image: Introducing Ziggy - a package providing Laravel Named Routes in JavaScript

Caleb Porzio and I started a podcast recently in our Tighten-provided 20% time . We call it Twenty Percent Time, and lately we've been talking a lot (#growthhacking, resources) about how we transfer data between our Laravel code and our JavaScript code. One thing we’ve come back to a few times is the desire to use the Laravel route() helper with named routes in JavaScript.

My JavaScript is full of axios calls, which are made to hard-coded API endpoint URLs in my Laravel apps. This can be a real pain when an endpoint route needs to be moved to a group with a URL prefix, when a parameter needs to be added, or when any other URL-breaking change needs to be made.

This is why, in Laravel, we have named routes, and why we don’t hard-code URLs in our Blade templates. By abstracting away the need for our consuming code to know the exact (down-to-the-letter) URL for a route, we are able to write more change-resilient code—and avoid a lot of “find and replace.”

We wanted the same kind of protection and convenience in JavaScript that we have inside our Laravel apps. And that’s why we built Ziggy.

Basic Usage

After you have Ziggy installed (composer require tightenco/ziggy and add the service provider to config/app.php), just drop our custom Blade directive (@routes) somewhere in your app’s Blade layout file before you load your main app JavaScript. Here's an example:

layout.blade.php

<!DOCTYPE html>
<html lang="en">
<head>
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{{ config('app.name', 'Laravel') }}</title>
<link rel="stylesheet" href="{{ mix('css/app.css') }}">
</head>
<body>
<div class="app">
</div>
@routes
<script src="{{ mix('js/app.js') }}"></script>
</body>
</html>

This Blade directive will be replaced with a <script> tag containing our route() helper function as well as a JavaScript object of your named routes, keyed by name. Let's assume this is what our routes file looks like:

routes/api.php

// Not named; shouldn't end up in output JSON
Route::get('/', 'HomeController');
 
// Named; should end up in output JSON
Route::group(['as' => 'posts.'], function () {
Route::get('/', 'PostsController@index')
->name('index');
Route::get('/{post}', 'PostsController@show')
->name('show');
Route::post('/', 'PostsController@store')
->name('store');
});
 
Route::get('/{post}/comments', 'PostCommentsController@index')
->name('post-comments.index');

Here's the actual code this would output (truncated to just show the first named route):

Output from @routes

<script>
var namedRoutes = JSON.parse(
'{"posts.index":{"uri":"posts","methods":["GET","HEAD"]}}'
);
// below, the route() helper is defined
</script>

As you can see, we've added a window.namedRoutes object you can access directly; it's an object that contains a series of objects, one for each named route. Those objects are keyed by their route name, so you could just directly ask for that route in your code: var routeObject = window.namedRoutes['posts.index'];

But you can also use our new route() helper, which mimics the same helper function in Laravel's PHP:

var routeUrl = route('posts.index');
var routeUrl = route('posts.show', {post: 1337});

Here's what it might look like in your real code:

// Returns results from /posts
return axios.get(route('posts.index'))
.then((response) => {
return response.data;
});
 
// Returns results from /posts/1
return axios.get(route('posts.show', {post: postId}))
.then((response) => {
return response.data;
});

You’ll notice that the route() helper takes two arguments: - A required Route name - An optional object full of parameters

The posts.show route, for example, requires a post parameter, so we pass {post: postId} as the second argument to route.

What’s next?

This is very much a first iteration of this concept. We have lots of features still to come.

Here are a few ideas we're already considering:

  • Filtering routes. We know you won’t always want export every named route, so we have some options coming for ways to filter which routes are and aren't sent to the frontend. (If you ask me, this feature is for convenience, not security. Your app’s security should not depend on hax0rz never guessing your URLs 😈)
  • Passing down full resources. We'd like to pass more than just route information—we'd like to pass information about your actual resources. If you want to ride the resource train you should probably check out the this episode of Twenty Percent Time.
  • An optional endpoint for routes. This would allow the client to grab the list of routes once from the server and cache them in memory.

Other ways to skin this cat

I've mentioned this need on Twitter and the podcast and had a few people reach out with other people who have worked on different solutions. The most similar package is called laroute, which I hoped would be the solution when I discovered it.

Here's why we decided to go forward with Ziggy anyway: laroute has over a dozen open pull requests and doesn't seem to have been updated recently except to support new versions of Laravel, but more importantly, it works differently on a foundational level: it requires you to re-publish your JavaScript every time you make a change to any of your routes. That's just not a cost we were willing to introduce to our development workflow, so we set out to do it more simply.

So, we're moving forward with Ziggy, but we also want to send some love in laroute's direction. If you need a package that provides a ton more functions than just our route() helper and you don't mind publishing your JavaScript every time you change your routes, definitely check out laroute.

There were two other Gists folks sent me that I wanted to acknowledge here:

Both Jeff and Clayton shared their solutions with me recently, and I like them both. If you'd rather roll your own, I suggest checking out these Gists for inspiration.

Conclusion

Here's the TL;DR: bring in Ziggy to your Laravel applications and put the @routes directive in one of your global templates, and you'll instantly get a route() helper that you can use just like the one included with Laravel. Now, your aliases go the whole way from your routes file to your JavaScript code, and a change in one will instantly bring a change in the other.

Get our latest insights in your inbox:

By submitting this form, you acknowledge our Privacy Notice.

Hey, let’s talk.

By submitting this form, you acknowledge our Privacy Notice.

This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.

Thank you!

We appreciate your interest. We will get right back to you.