Typed routes from Laravel to React with Wayfinder original
by Freek Van der Herten – 3 minute read – Edit
I've been writing about There There a lot lately. It's a helpdesk we're building at Spatie, born from two decades of running our own customer support for our open source work and SaaS apps. After all that time being the customer of other helpdesks, we finally started on the one we actually wanted to use.
Our take on AI here is to aim it at the support agent, not the customer. The human owns the conversation. The model makes the reply a little cleaner and a little faster. There There is in private beta, and you can apply for early access at there-there.app.
We're building There There with Laravel and Inertia. One package we've come to rely on is Laravel Wayfinder. It generates typed TypeScript functions for every route and controller in your Laravel app, so the frontend talks to the backend without a single hardcoded URL. This post is about why that matters.
Using Wayfinder
A React codebase quickly fills up with URL strings. Every fetch, every form action, every link points to a route by way of a template literal. Then you rename a route in Laravel, or add a required parameter, and nothing breaks at compile time. You find out in production, or when a teammate clicks the wrong thing.
Wayfinder fixes this by generating a function per named route. You import it and call .url() to build the URL.
Here's a snippet from our team membership toggle.
import { add as addTeamMember, remove as removeTeamMember, } from '@/routes/app/settings/workspace/teams/members'; removeTeamMember.url({ team: team.ulid, user: member.ulid }); addTeamMember.url({ team: team.ulid, user: member.ulid });
Two things are worth pointing out. First, the parameters are type-checked. If you rename the user route parameter to member, TypeScript catches every caller. Second, the route names map cleanly to import paths, so finding them is just autocomplete.
Wayfinder also generates a typed function per controller. We use that when we talk to an endpoint via plain fetch, outside of an Inertia visit. Here's the retry action for a failed outbound message.
import MessageRetryController from '@/actions/App/Http/Controllers/Tickets/MessageRetryController'; await fetch( MessageRetryController.url({ ticket: current.ulid, message: message.ulid }), { method: 'POST' }, );
Same deal. The URL is type-safe, the parameters are checked, and renaming the controller in PHP breaks the TypeScript build rather than production.
To make all of this work, we hook Wayfinder into Vite. Here's the relevant bit of our vite.config.js.
import { wayfinder } from "@laravel/vite-plugin-wayfinder"; export default defineConfig({ plugins: [ // ... wayfinder(), ], });
The plugin watches your Laravel routes and controllers during development, and regenerates the typed functions whenever something changes. Imports stay in sync with your backend without any manual step.

In closing
Wayfinder has shaved a whole category of bug off our work. No more grep-the-codebase when a route changes, no more "why is this fetch returning 404", no more arguing about trailing slashes. You write the route in Laravel, import it in TypeScript, and the two stay honest with each other.
You can read more about it in the Wayfinder repo on GitHub. And if you'd like to try There There yourself, we're in private beta right now and you can apply for early access at there-there.app.