# Laravel Wayfinder Laravel Wayfinder bridges your Laravel backend and TypeScript frontend with zero friction. It automatically generates fully-typed, importable TypeScript functions for your controllers and routes, allowing you to call Laravel endpoints directly in your client code just like any other function. No more hardcoding URLs, guessing route parameters, or manually syncing backend changes. Wayfinder generates TypeScript definitions in three directories (`wayfinder`, `actions`, and `routes`) within `resources/js`. The generated functions return objects containing the resolved URL and HTTP method, making them perfect for use with Inertia.js, fetch API, or conventional HTML forms. The package supports invokable controllers, named routes, route model binding, optional parameters, query parameters, and URL defaults through middleware. ## Installation Install Wayfinder via Composer and the Vite plugin via NPM to enable automatic TypeScript generation during development. ```bash # Install the Laravel package composer require laravel/wayfinder # Install the Vite plugin npm i -D @laravel/vite-plugin-wayfinder ``` ```ts // vite.config.js import { wayfinder } from "@laravel/vite-plugin-wayfinder"; export default defineConfig({ plugins: [ wayfinder(), // ... other plugins ], }); ``` ## Generate TypeScript Definitions The `wayfinder:generate` Artisan command creates TypeScript definitions for all your routes and controller methods with full type safety. ```bash # Generate all TypeScript definitions php artisan wayfinder:generate # Specify custom output path php artisan wayfinder:generate --path=resources/js/wayfinder # Skip controller actions generation php artisan wayfinder:generate --skip-actions # Skip named routes generation php artisan wayfinder:generate --skip-routes # Include form variants for HTML forms php artisan wayfinder:generate --with-form ``` ## Basic Controller Action Usage Import and call generated functions for controller methods. Each function returns an object with `url` and `method` properties, or use `.url()` to get just the URL string. ```ts import { show, index, store, update, destroy } from "@/actions/App/Http/Controllers/PostController"; // Basic usage - returns { url: string, method: string } index(); // { url: "/posts", method: "get" } show(1); // { url: "/posts/1", method: "get" } show({ post: 1 }); // { url: "/posts/1", method: "get" } // Get just the URL show.url(1); // "/posts/1" index.url(); // "/posts" // Specify HTTP method explicitly show.get(1); // { url: "/posts/1", method: "get" } show.head(1); // { url: "/posts/1", method: "head" } update.patch({ post: 1 }); // { url: "/posts/1", method: "patch" } destroy.delete({ post: 1 }); // { url: "/posts/1", method: "delete" } // Access route definition metadata show.definition; // { methods: ["get", "head"], url: "/posts/{post}" } ``` ## Multiple Route Parameters Pass multiple parameters as an array or object when routes have multiple placeholders. ```ts import { update } from "@/actions/App/Http/Controllers/PostController"; // Route: /posts/{post}/author/{author} // Using array (positional) update([1, 2]); // { url: "/posts/1/author/2", method: "patch" } // Using object (named) update({ post: 1, author: 2 }); // { url: "/posts/1/author/2", method: "patch" } // Using objects with id property update({ post: { id: 1 }, author: { id: 2 } }); // { url: "/posts/1/author/2", method: "patch" } ``` ## Route Model Binding When using custom route key bindings (e.g., `{post:slug}`), Wayfinder supports passing values by the bound key. ```ts import { show } from "@/actions/App/Http/Controllers/PostController"; // Route: /posts/{post:slug} // Direct value show("my-new-post"); // { url: "/posts/my-new-post", method: "get" } // Object with key property show({ slug: "my-new-post" }); // { url: "/posts/my-new-post", method: "get" } // Object with full model show({ id: 1, slug: "my-new-post" }); // Uses slug: { url: "/posts/my-new-post", method: "get" } ``` ## Invokable Controllers Import invokable controllers as default exports and call them directly as functions. ```ts import InvokableController from "@/actions/App/Http/Controllers/InvokableController"; import StorePostController from "@/actions/App/Http/Controllers/StorePostController"; // Call directly InvokableController(); // { url: "/invokable-controller", method: "get" } StorePostController(); // { url: "/posts", method: "post" } // Access URL InvokableController.url(); // "/invokable-controller" ``` ## Import Entire Controller Import a controller object to access all its methods, though this prevents tree-shaking. ```ts import PostController from "@/actions/App/Http/Controllers/PostController"; PostController.index(); // { url: "/posts", method: "get" } PostController.show(1); // { url: "/posts/1", method: "get" } PostController.store(); // { url: "/posts", method: "post" } PostController.update({ post: 1 }); // { url: "/posts/1", method: "patch" } PostController.destroy({ post: 1 }); // { url: "/posts/1", method: "delete" } ``` ## Named Routes Import generated functions based on Laravel's named routes for a cleaner API independent of controller structure. ```ts import { dashboard, invokable } from "@/routes"; import { show, edit } from "@/routes/posts"; // Route name: posts.show show(1); // { url: "/posts/1", method: "get" } show.url(1); // "/posts/1" // Route name: posts.edit edit(1); // { url: "/posts/1/edit", method: "get" } // Route name: dashboard dashboard(); // { url: "/dashboard", method: "get" } // Route name: invokable invokable(); // { url: "/named-invokable-controller", method: "get" } ``` ## Query Parameters Append query parameters to URLs using the `query` option, or merge with existing browser URL parameters using `mergeQuery`. ```ts import { show, index } from "@/actions/App/Http/Controllers/PostController"; // Basic query parameters index({ query: { page: 1, sort_by: "name" } }); // { url: "/posts?page=1&sort_by=name", method: "get" } show(1, { query: { page: 1, sort_by: "name" } }); // { url: "/posts/1?page=1&sort_by=name", method: "get" } // Works with all method variants show.url(1, { query: { page: 1 } }); // "/posts/1?page=1" show.get(1, { query: { page: 1 } }); // { url: "/posts/1?page=1", method: "get" } // Array query parameters index({ query: { tags: ["php", "laravel"] } }); // { url: "/posts?tags[]=php&tags[]=laravel", method: "get" } // Nested object parameters index({ query: { filters: { status: "active", category: "tech" } } }); // { url: "/posts?filters[status]=active&filters[category]=tech", method: "get" } // Boolean conversion index({ query: { featured: true, archived: false } }); // { url: "/posts?featured=1&archived=0", method: "get" } ``` ## Merge Query Parameters Use `mergeQuery` to combine new parameters with the current browser URL's query string, useful for pagination and filters. ```ts import { index } from "@/actions/App/Http/Controllers/PostController"; // Current URL: /posts?page=1&sort_by=category&q=shirt // Merge with existing params index({ mergeQuery: { page: 2, sort_by: "name" } }); // { url: "/posts?page=2&sort_by=name&q=shirt", method: "get" } // Remove params by setting to null or undefined index({ mergeQuery: { page: 2, sort_by: null } }); // { url: "/posts?page=2&q=shirt", method: "get" } // Merge array params (replaces existing array) index({ mergeQuery: { tags: ["new", "featured"] } }); // Replaces any existing tags[] params ``` ## HTML Form Support Generate form attributes for conventional HTML form submissions when using the `--with-form` flag. ```bash php artisan wayfinder:generate --with-form ``` ```tsx import { store, update } from "@/actions/App/Http/Controllers/PostController"; // POST form const CreateForm = () => (
); // PATCH form (uses method spoofing) const EditForm = ({ postId }: { postId: number }) => ( ); // Specify HTTP method explicitly const PutForm = ({ postId }: { postId: number }) => ( ); // Form with query parameters const FormWithQuery = () => ( ); ``` ## URL Defaults Set default URL parameters from the frontend that apply to all generated routes, useful for locale prefixes or tenant domains. ```ts import { setUrlDefaults, addUrlDefault } from "@/wayfinder"; import { onlyDefaults, mixedDefaults } from "@/actions/App/Http/Controllers/UrlDefaultsController"; // Route: /with-defaults/{locale} // Middleware sets locale default to "en" onlyDefaults(); // { url: "/with-defaults/en", method: "post" } onlyDefaults({ locale: "es" }); // { url: "/with-defaults/es", method: "post" } // Route: /with-defaults/{locale}/also/{timezone} mixedDefaults({ timezone: "UTC" }); // { url: "/with-defaults/en/also/UTC", method: "post" } mixedDefaults({ timezone: "UTC", locale: "es" }); // { url: "/with-defaults/es/also/UTC", method: "post" } // Set defaults from frontend setUrlDefaults({ locale: "fr", tenant: "acme" }); // Or set dynamically with a function setUrlDefaults(() => ({ locale: localStorage.getItem("locale") || "en", })); // Add a single default addUrlDefault("locale", "de"); ``` ## Inertia.js Integration Wayfinder integrates seamlessly with Inertia.js forms and links, automatically resolving URLs and methods. ```tsx import { useForm } from "@inertiajs/react"; import { Link } from "@inertiajs/react"; import { store, show, index } from "@/actions/App/Http/Controllers/PostController"; // With useForm - submit method auto-resolves URL and method const CreatePost = () => { const form = useForm({ title: "", content: "", }); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); form.submit(store()); // POST to /posts }; return ( ); }; // With Link component const Navigation = () => ( ); ``` ## Reserved JavaScript Keywords Controller methods using JavaScript reserved words (like `delete`, `import`, `default`) are automatically renamed with a `Method` suffix. ```ts import { deleteMethod, importMethod, defaultMethod } from "@/actions/App/Http/Controllers/ResourceController"; // PHP: public function delete() -> TypeScript: deleteMethod deleteMethod({ id: 1 }); // { url: "/resources/1", method: "delete" } // PHP: public function import() -> TypeScript: importMethod importMethod(); // { url: "/resources/import", method: "post" } // PHP: public function default() -> TypeScript: defaultMethod defaultMethod(); // { url: "/resources/default", method: "get" } ``` ## TypeScript Types Wayfinder exports TypeScript types for route definitions and query options. ```ts import type { RouteDefinition, RouteFormDefinition, RouteQueryOptions, QueryParams } from "@/wayfinder"; // RouteDefinition - returned by action functions type PostShowResult = RouteDefinition<"get">; // { url: string; method: "get" } type PostStoreResult = RouteDefinition<"post">; // { url: string; method: "post" } // RouteFormDefinition - returned by .form() methods type FormResult = RouteFormDefinition<"post">; // { action: string; method: "post" } // RouteQueryOptions - options parameter type const options: RouteQueryOptions = { query: { page: 1 }, // or mergeQuery: { sort: "desc" }, }; // QueryParams - nested query parameter structure const params: QueryParams = { simple: "value", number: 42, boolean: true, array: ["a", "b"], nested: { deep: { value: "found", }, }, }; ``` ## Summary Laravel Wayfinder is designed for modern Laravel applications with TypeScript frontends, particularly those using Inertia.js, React, Vue, or Svelte. The primary use cases include eliminating hardcoded URLs in frontend code, ensuring type-safe route parameters with IDE autocompletion, and maintaining synchronization between backend routes and frontend API calls. The package is especially valuable for teams working on large applications where route changes would otherwise require manual updates across multiple frontend files. Integration patterns center around importing generated functions directly into components and using them with fetch, Axios, Inertia's `useForm`, or HTML forms. The Vite plugin ensures definitions regenerate automatically during development, while the `.gitignore`-friendly output directories mean generated files don't clutter version control. For applications with locale prefixes, tenant subdomains, or other URL defaults, the middleware integration and frontend `setUrlDefaults` function provide flexible solutions that work seamlessly with Laravel's URL generation system.