README
View SourceIntroduction
Phoenix Wayfinder seamlessly connects your Phoenix backend with your TypeScript frontend. It automatically generates fully-typed, importable TypeScript functions for your controllers, allowing you to call your Phoenix endpoints directly from your client code as if they were regular functions. No more hardcoded URLs, guessing route parameters, or manually syncing backend changes.
You may notice that this README is very similar to Laravel's version of Wayfinder. That is intentional! The goal is to provide a consistent experience across frameworks. This package adapts the great ideas from the Laravel version to the Phoenix ecosystem, so you can enjoy the same benefits in your Phoenix applications.
Thank you to the Laravel team for the inspiration! 🙌
[!IMPORTANT] Wayfinder is currently in Beta. The API may change before the v1.0.0 release. All notable changes will be documented in the changelog.
Table of Contents
- Installation
- Configuration
- Router Setup
- Development Setup
- Production Setup
- TypeScript Setup
- Vite Users
- Ignoring Generated Files
- Prettier Configuration
- Generated Files
- Usage
- Checking Current URL
- Contributing
- License
Installation
To get started, install Wayfinder using the Composer package manager:
defp deps do
[
{:wayfinder_ex, "~> 0.1.0"}
]
end
Then, run the following command to fetch the dependencies:
mix deps.get
Configuration
Configure Wayfinder in your config/config.exs
file. This configuration specifies which OTP app Wayfinder belongs to and which router to use for generating the TypeScript functions.
defmodule MyApp.Router do
use MyApp, :router
+ use Wayfinder.PhoenixRouter
# ...rest of your router code
end
Development Setup
For development, you can enable the Wayfinder.RoutesWatcher
to automatically reload routes when they change. This is useful during development to avoid restarting the server every time you modify your routes.
defmodule MyApp.Application do
def start(_type, _args) do
children = [
MyApp.Telemetry,
MyApp.Repo,
{DNSCluster, query: Application.get_env(:my_app, :dns_cluster_query) || :ignore},
{Phoenix.PubSub, name: MyApp.PubSub},
# ... other children
]
children =
if Mix.env() == :dev do
+ children ++ [{Wayfinder.RoutesWatcher, []}]
else
children
end
opts = [strategy: :one_for_one, name: MyApp.Supervisor]
Supervisor.start_link(children, opts)
end
# ...rest of your application code
end
Production Setup
In production, you need to run the Wayfinder mix task to generate fresh TypeScript functions.
# mix.exs
defmodule MyApp.MixProject do
use Mix.Project
defp aliases do
[
# ...other aliases
+ "assets.build": ["wayfinder.generate", "cmd pnpm --dir assets run build"],
"assets.deploy": ["assets.build", "phx.digest"]
]
end
end
TypeScript Setup
It is recommended to use aliases in your project so you can reference Wayfinder helpers and actions without using relative paths.
{
"compilerOptions": {
"paths": {
"@/*": ["./js/*"]
}
}
}
Vite Users
If you are using Vite, you can set the same alias as follows:
import { resolve } from 'node:path'
import { defineConfig } from 'vite'
export default defineConfig({
resolve: {
alias: {
'@': resolve(__dirname, './js'),
},
},
})
Once this is set up, you can import the generated TypeScript functions in your client code like this:
import UsersController from '@/actions/UsersController'
// Or import only one action (recommended for better tree-shaking)
import { create } from '@/actions/UsersController'
You can see a full example in this example project.
Ignoring Generated Files
It is recommended to configure your project to ignore generated files.
ESLint Configuration
Generated TypeScript from Wayfinder might not follow your project's ESLint
rules. It is recommended to ignore the generated files in your assets/eslint.config.js
file:
import tseslint from 'typescript-eslint'
export default tseslint.config(
{ ignores: ['dist', 'js/actions/**', 'js/wayfinder/**'] },
{
// ... your other ESLint rules
},
)
Prettier Configuration
Similarly, you may want to ignore the generated files in your assets/.prettierignore
file:
js/actions/**/*
js/wayfinder/**/*
Generated Files
When Wayfinder is set up and you change your routes or run mix wayfinder.generate
, it will generate two new directories in your assets/js
folder:
assets/
└── js/
├── wayfinder/ # Shared helper functions (e.g., .url(), .visit(), etc.)
└── actions/ # Auto-generated route handlers
├── UsersController/
│ └── index.ts # Handles routes for UsersController#index
├── HomeController/
│ └── index.ts # Handles routes for HomeController#index
└── Admin/
└── TasksController/
└── index.ts # Handles routes for Admin::TasksController#index
Usage
Now that Wayfinder is set up and you know how to import the generated TypeScript functions, let's see how to use them in your client code. Generated functions are typed with the parameters you defined in your Phoenix router. For example:
defmodule MyApp.Router do
use MyApp, :router
+ use Wayfinder.PhoenixRouter
resources("/users", UsersController)
end
We defined a full CRUD resource for users, so Wayfinder will generate the following TypeScript functions:
// assets/js/actions/UsersController/index.ts
const UsersController = {
create,
delete: deleteMethod,
index,
show,
update,
edit,
new: newMethod,
}
export default UsersController
Suppose you want to edit a user. You can use the edit
function like this with
Inertia's useForm:
import { edit } from '@/actions/UsersController'
function EditUser({ name, id }: { id: number; name: string; }) {
const { data, setData, post, processing, errors } = useForm({
id,
name,
})
function submit(e) {
e.preventDefault()
// .url is typed. You can only pass an `id` here. It can be a string or number
+ post(edit.url({ id: data.id }))
// Alternatively, you can pass just the id. This is equivalent to the above
// post(edit.url(data.id))
}
return (
<form onSubmit={submit}>
<input type="text" value={data.name} onChange={e => setData('name', e.target.value)} />
<button type="submit" disabled={processing}>Login</button>
</form>
)
}
[!IMPORTANT] We have tried to support all ways of defining routes in Phoenix, including glob routes like
get "/something/*path", SomethingController, :index
.
[!IMPORTANT] Phoenix does not support optional parameters, but if you define the same route with and without a parameter, Wayfinder will generate both functions for you. For example:
defmodule MyApp.Router do
use MyApp, :router
use Wayfinder.PhoenixRouter
get "/something", SomethingController, :show
get "/something/:my_parameter", SomethingController, :show
end
Generated TypeScript functions will be:
import { show } from '@/actions/SomethingController'
// This is valid
show.url() + // No parameters
// This is also valid
show.url({ my_parameter: 'value' }) // With parameter
Checking Current URL
If you need to know which page you are on, Inertia's usePage
hook can help. It also works for SSR-rendered pages.
import { usePage } from '@inertiajs/react'
import { Menu } from '@/components/Menu'
import { home } from '@/actions/HomeController'
import { organizations } from '@/actions/OrganizationsController'
import { contacts } from '@/actions/ContactsController'
import { reports } from '@/actions/ReportsController'
function MyMenu() {
const { url: currentPath } = usePage()
return (
<div className={className}>
<MenuItem
text='Dashboard'
link={home.url({ currentPath, exactMatch: true })}
/>
<MenuItem
text='Organizations'
link={organizations.url({ currentPath })}
icon={<Building size={20} />}
/>
<MenuItem
text='Contacts'
link={contacts.url({ currentPath })}
icon={<Users size={20} />}
/>
<MenuItem
text='Reports'
link={reports.url({ currentPath })}
icon={<Printer size={20} />}
/>
</div>
)
}
[!IMPORTANT] The
currentPath
parameter is optional, but it helps Wayfinder generate the correct URL for the current page. If you don't pass it, Wayfinder will generate a URL without the current path.
[!IMPORTANT] You can pass
exactMatch: true
to thehome.url()
function. This will generate a URL that matches the current path exactly, so you can use it to highlight the current page in your menu. The home menu item will only be selected if the current path is exactly/
.
Contributing
Thank you for considering contributing to Elixir Wayfinder! You can read the contribution guide here.
License
Wayfinder is open-source software licensed under the MIT license.