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.
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.
- Installation
- Configuration
- Router Setup
- Development Setup
- Production Setup
- TypeScript Setup
- Vite Users
- Ignoring Generated Files
- Prettier Configuration
- Generated Files
- Usage
- Checking Current URL
- Why the Name "Wayfinder"?
- Contributing
- License
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
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.
# config/config.exs
config :wayfinder_ex,
otp_app: :my_app,
router: MyApp.Router,
# Optional: Specify paths to ignore when generating routes
# ignore_paths: ["^/backoffice"]
# lib/my_app_web/router.ex
defmodule MyApp.Router do
use MyApp, :router
+ use Wayfinder.PhoenixRouter
# ...rest of your router code
end
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
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
It is recommended to use aliases in your project so you can reference Wayfinder helpers and actions without using relative paths.
{
"compilerOptions": {
"paths": {
"@/*": ["./js/*"]
}
}
}
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.
It is recommended to configure your project to ignore generated files.
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
},
)
Similarly, you may want to ignore the generated files in your assets/.prettierignore
file:
js/actions/**/*
js/wayfinder/**/*
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
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
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 the home.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 /
.
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! 🙌
Thank you for considering contributing to Elixir Wayfinder! You can read the contribution guide here.
Wayfinder is open-source software licensed under the MIT license.