Laravel Wayfinder: Type Safe Routes and Forms with Inertia
Laravel Wayfinder's new beta goes beyond route generation, it creates TypeScript types from your form requests, models, and enums. Here's how to set it up with Inertia.
If you've built a full stack Laravel app with Inertia, you know the pain. You rename a route, forget to update your React component, and spend twenty minutes wondering why your form submission is hitting a 404. Or you add a field to your form request validation, but your frontend is still sending the old payload structure.
Laravel Wayfinder fixes this. It reads your PHP code and generates TypeScript that stays in sync with your backend automatically. Routes, form request types, model types, enums everything your frontend needs to know about your backend gets generated and typed.
Here's the thing that got my attention: as of early January 2026, Wayfinder shipped a major new beta that goes way beyond route generation. The previous version (which replaced Ziggy in Laravel Starter Kits) only handled routes. This new version generates TypeScript from form requests, Eloquent models, PHP enums, Inertia props, broadcast channels, and more.
Let me show you how to set it up and why the form request integration alone makes this worth using.
Wayfinder vs Ziggy: What's Different?
Before Wayfinder, most of us used Ziggy for route generation. Ziggy did one thing well: it let you call route('posts.show', { post: 1 }) in your JavaScript and get back the URL. That's it.
Wayfinder takes a completely different approach. Instead of just mapping route names to URLs, it analyzes your entire Laravel codebase and generates TypeScript that mirrors your PHP structures.
| Feature | Ziggy | Wayfinder |
|---|---|---|
| Route URLs | ✓ | ✓ |
| HTTP method awareness | ✗ | ✓ |
| Form request types | ✗ | ✓ |
| Eloquent model types | ✗ | ✓ |
| PHP enum constants | ✗ | ✓ |
| Inertia page props | ✗ | ✓ |
| Broadcast channels | ✗ | ✓ |
| Environment variables | ✗ | ✓ |
The route generation alone is more powerful than Ziggy. Wayfinder generates actual TypeScript functions for each controller method, complete with typed parameters. But the real value is everything else particularly form request types.
As of Laravel 12, Wayfinder replaced Ziggy in the official Starter Kits. So if you're starting a new Laravel project with Inertia, you're already set up with the stable version. This tutorial covers the new beta with expanded features.
Installation and Setup
First, install the beta version via Composer:
composer require laravel/wayfinder:dev-next
Publish the configuration file to customize what gets generated:
php artisan vendor:publish --tag=wayfinder-config
Next, add the Vite plugin to vite.config.js. This handles automatic regeneration when your PHP files change:
import { wayfinder } from "@laravel/vite-plugin-wayfinder";
export default defineConfig({
plugins: [
laravel({
input: ['resources/js/app.tsx'],
refresh: true,
}),
wayfinder(),
react(),
],
});
Generate the TypeScript files:
php artisan wayfinder:generate
By default, Wayfinder outputs everything to resources/js/wayfinder. If you want a different location:
php artisan wayfinder:generate --path=resources/ts/api
Here's what most tutorials won't mention: once you have the Vite plugin configured, you don't need to manually run the generate command during development. When Vite's dev server is running, Wayfinder watches for changes to your PHP files and regenerates automatically. Change a route, update a form request—your TypeScript updates within seconds.
You can safely add the wayfinder directory to your .gitignore since it regenerates on every build.
Route Generation Basics
Let's start with a typical controller:
class PostController
{
public function index() { /* ... */ }
public function show(Post $post) { /* ... */ }
public function store(StorePostRequest $request) { /* ... */ }
public function update(UpdatePostRequest $request, Post $post) { /* ... */ }
public function destroy(Post $post) { /* ... */ }
}
Wayfinder generates a TypeScript module you can import and call like any function:
import { PostController } from "@/wayfinder/App/Http/Controllers/PostController";
// Returns both URL and HTTP method
PostController.index();
// { url: '/posts', method: 'get' }
// Routes with parameters are typed
PostController.show({ post: 1 });
// { url: '/posts/1', method: 'get' }
PostController.update({ post: 42 });
// { url: '/posts/42', method: 'put' }
Need just the URL string? Each function has a .url() method:
PostController.show.url({ post: 42 });
// '/posts/42'
Query parameters work too:
PostController.index({ query: { page: 2, sort: "created_at" } });
// { url: '/posts?page=2&sort=created_at', method: 'get' }
If you prefer named routes (like Laravel's route() helper), Wayfinder generates those as well:
import posts from "@/wayfinder/routes/posts";
posts.index();
posts.show({ post: 1 });
The TypeScript compiler catches parameter errors at build time. Try calling PostController.show() without the post parameter and you'll see an error before your code ever runs.
Form Handling with Inertia
This is where Wayfinder becomes genuinely useful for real applications.
HTML forms only support GET and POST methods. When you need PUT, PATCH, or DELETE, Laravel uses method spoofing a hidden _method field that tells the framework what you actually want. Wayfinder handles this automatically with the .form variant:
PostController.update.form({ post: 1 });
// { action: '/posts/1?_method=PATCH', method: 'post' }
PostController.destroy.form({ post: 1 });
// { action: '/posts/1?_method=DELETE', method: 'post' }
You can spread these directly onto your form element in React:
<form {...PostController.update.form({ post: post.id })}>
{/* fields */}
</form>
But here's where it gets really good: form request validation rules become TypeScript types.
Given this Laravel form request:
class StorePostRequest extends FormRequest
{
public function rules(): array
{
return [
'title' => ['required', 'string', 'max:255'],
'body' => ['required', 'string'],
'excerpt' => ['nullable', 'string'],
'tags' => ['nullable', 'array'],
'tags.*' => ['string'],
'meta' => ['nullable', 'array'],
'meta.description' => ['nullable', 'string'],
];
}
}
Wayfinder generates this in types.d.ts:
export namespace App.Http.Controllers.PostController.Store {
export type Request = {
title: string;
body: string;
excerpt?: string | null;
tags?: string[] | null;
meta?: {
description?: string | null;
} | null;
};
}
Notice how it handles nullable fields (marked as optional with ?), arrays, and even nested objects. The tags.* rule becomes string[]. The meta.description rule becomes a nested object type.
Now use it with Inertia's useForm:
import { App } from "@/wayfinder/types";
import { useForm } from '@inertiajs/react';
const form = useForm<App.Http.Controllers.PostController.Store.Request>({
title: '',
body: '',
excerpt: null,
tags: [],
meta: { description: null },
});
TypeScript now knows exactly what fields are valid and their types. Add a field in your form request validation, regenerate Wayfinder, and TypeScript will error if you haven't updated your frontend form.
Here's a complete component example:
import { App } from "@/wayfinder/types";
import { PostController } from "@/wayfinder/App/Http/Controllers/PostController";
import { useForm } from '@inertiajs/react';
export function CreatePost() {
const form = useForm<App.Http.Controllers.PostController.Store.Request>({
title: '',
body: '',
excerpt: null,
tags: [],
});
const submit = (e: React.FormEvent) => {
e.preventDefault();
form.post(PostController.store.url());
};
return (
<form onSubmit={submit}>
<input
name="title"
value={form.data.title}
onChange={e => form.setData('title', e.target.value)}
/>
{form.errors.title && <span>{form.errors.title}</span>}
<textarea
name="body"
value={form.data.body}
onChange={e => form.setData('body', e.target.value)}
/>
{/* TypeScript error if you use wrong field name */}
<button type="submit" disabled={form.processing}>
Create Post
</button>
</form>
);
}
The key insight: your validation rules are defined once in PHP. TypeScript types are generated automatically. No more manual type definitions that drift from your backend. When you add a new validation rule or change an existing one, your frontend types update on the next build.
Model Types
Wayfinder analyzes your Eloquent models and generates TypeScript interfaces for them. Given:
class Post extends Model
{
protected $casts = [
'published_at' => 'datetime',
'is_featured' => 'boolean',
];
public function author(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function tags(): BelongsToMany
{
return $this->belongsToMany(Tag::class);
}
}
Wayfinder generates:
export namespace App.Models {
export type Post = {
id: number;
title: string;
body: string;
published_at: string | null;
is_featured: boolean;
author: App.Models.User;
tags: App.Models.Tag[];
created_at: string;
updated_at: string;
};
}
The casts matter. datetime becomes string | null (since it serializes to ISO format). boolean stays boolean. Relationships are typed based on their return type.
Use these in your components:
import { App } from "@/wayfinder/types";
interface PostCardProps {
post: App.Models.Post;
}
export function PostCard({ post }: PostCardProps) {
return (
<article>
<h2>{post.title}</h2>
<p>By {post.author.name}</p>
{post.is_featured && <span className="badge">Featured</span>}
</article>
);
}
Full autocomplete in your IDE. Type errors if you access a property that doesn't exist.
PHP Enums
PHP 8.1 enums translate directly to TypeScript. Given:
enum PostStatus: string
{
case Draft = 'draft';
case Published = 'published';
case Archived = 'archived';
}
Wayfinder generates both a type (for type checking) and constants (for runtime use):
// In types.d.ts
export namespace App.Enums {
export type PostStatus = "draft" | "published" | "archived";
}
// In App/Enums/PostStatus.ts
export const Draft = "draft";
export const Published = "published";
export const Archived = "archived";
export const PostStatus = { Draft, Published, Archived } as const;
export default PostStatus;
Use the constants in your code:
import PostStatus from "@/wayfinder/App/Enums/PostStatus";
import { App } from "@/wayfinder/types";
interface Props {
post: App.Models.Post & { status: App.Enums.PostStatus };
}
export function PostStatusBadge({ post }: Props) {
if (post.status === PostStatus.Published) {
return <span className="badge-green">Live</span>;
}
if (post.status === PostStatus.Draft) {
return <span className="badge-yellow">Draft</span>;
}
return <span className="badge-gray">Archived</span>;
}
Add a new enum case in PHP, regenerate, and TypeScript will tell you everywhere you need to handle it.
Should You Use It Now?
Let me be direct about the current state. The new Wayfinder is what Joe Tannenbaum called an "uppercase B beta." The API may change before v1.0. Performance on very large codebases still needs optimization. Some edge cases with complex type inference aren't fully covered yet.
That said, here's my take:
For new Inertia projects: Yes, use it. The form request types alone will save you hours of debugging. The risk of breaking changes is outweighed by the productivity gains, and refactoring to accommodate API changes is straightforward.
For existing projects: Evaluate your situation. If you have a medium sized codebase and you're already dealing with frontend/backend sync issues, it's worth trying. If you're risk-averse or have a massive monolith, maybe wait for stable.
What I'd skip for now: If you're interested in the broadcast channel/event types or multi-repo syncing (which is genuinely impressive), those features work but are more complex to set up. Focus on routes and form requests first.
The team is actively collecting feedback on GitHub. If you run into issues, report them they're responsive and the package is improving rapidly.
What's Not Covered Here
This post focused on the features I think will matter most for everyday Inertia development. Wayfinder does more that I didn't cover:
- Broadcast channels and events for Laravel Echo WebSocket integration with typed payloads
- Cross-repository syncing via GitHub Actions to keep separate frontend/backend repos in sync
- Inertia shared props typing from your
HandleInertiaRequestsmiddleware - Vite environment variable types for
import.meta.env
I'll cover some of these in future posts once the API stabilizes.
Resources
- Wayfinder GitHub Repository (next branch) — The beta version with all new features
- Laravel Blog Announcement — Official announcement with livestream recording
- Inertia.js Forms Documentation — For the
useFormhook reference
FAQ
What is Laravel Wayfinder?
Laravel Wayfinder is an official Laravel package that generates TypeScript from your PHP code. It analyzes your controllers, routes, form requests, models, and enums to create fully-typed TypeScript that stays in sync with your backend automatically.
What's the difference between Wayfinder and Ziggy?
Ziggy only generates route URLs from route names. Wayfinder generates route URLs plus HTTP methods, form request types, model types, PHP enum constants, Inertia page props, broadcast channels, and more. Wayfinder has replaced Ziggy in Laravel's official Starter Kits as of Laravel 12.
Does Wayfinder work with Vue and React?
Yes. Wayfinder generates framework-agnostic TypeScript that works with any frontend. It has specific integrations for Inertia.js (both Vue and React adapters) including typed page props and shared data. If you're using Laravel Echo with Vue or React, it can also generate typed event handlers.
Is Wayfinder ready for production?
The new version is in public beta. The previous version (route generation only) is stable and ships with Laravel Starter Kits. For the beta features (form requests, models, enums), the API may change before v1.0, but it's functional and actively maintained. I'd use it for new projects; for existing production apps, evaluate based on your risk tolerance.
Need Help With Your Laravel Project?
I specialize in building custom Laravel applications, process automation, and SaaS development. Whether you need to eliminate repetitive tasks or build something from scratch, let's discuss your project.
⚡ Currently available for 2-3 new projects
About Hafiz Riaz
Full Stack Developer from Turin, Italy. I build web applications with Laravel and Vue.js, and automate business processes. Creator of ReplyGenius, StudyLab, and other SaaS products.
View Portfolio →Get web development tips via email
Join 50+ developers • No spam • Unsubscribe anytime
Related Articles
Setting Up Laravel 10 with Vue3 and Vuetify3
A complete guide to seamlessly integrating Vue3 and Vuetify3 into Laravel 10 usi...
Effortlessly Dockerize Your Laravel & Vue Application: A Step-by-Step Guide
Unleash the Full Potential of Laravel 10 with Vue 3 and Vuetify 3 in a Dockerize...
Mastering Design Patterns in Laravel
Unraveling the Secrets Behind Laravel’s Architectural Mastery