Use your named AdonisJS routes in JavaScript — like Ziggy does for Laravel, but for AdonisJS and Inertia.
It gives you a route() helper on the client with full TypeScript inference: route names are autocompleted, required parameters are enforced at compile time, and you never hardcode a URL again.
Works with AdonisJS v6 and v7.
node ace add @izzyjs/routeThat single command installs the package and configures everything: the provider, the middleware, the Japa plugin, the config/izzyjs.ts file, and an initial route generation.
Manual setup
npm install @izzyjs/route
node ace configure @izzyjs/routeThe configure step does the same wiring as node ace add.
Route definitions (and their TypeScript types) are generated from your named routes:
node ace izzy:routesTo regenerate automatically whenever the dev server starts, register the dev hook in adonisrc.ts:
// adonisrc.ts — AdonisJS v7 (assembler v8){hooks: {devServerStarted: [()=>import('@izzyjs/route/dev_hook')],},}// adonisrc.ts — AdonisJS v6 (assembler v7){unstable_assembler: {onDevServerStarted: [()=>import('@izzyjs/route/dev_hook')],},}Add the @routes() tag to your Edge layout, before your app scripts:
<!-- resources/views/inertia_layout.edge --><!doctype html><html><head>
@routes()
@vite(['resources/js/app.js'])
</head><body>
@inertia()
</body></html>This injects your (filtered) route list into the page so route() can resolve names at runtime.
config/izzyjs.ts is created for you. baseUrl is required — it's what powers the url property (complete URLs with protocol and domain):
// config/izzyjs.tsimport{defineConfig}from'@izzyjs/route'exportdefaultdefineConfig({baseUrl: process.env.APP_URL||'http://localhost:3333',routes: {// Pick one: `only` or `except`. Setting both disables filtering.// only: ['home', 'posts.*'],except: ['_debugbar.*','admin.*'],// Optional named groups (see "Route groups" below)groups: {admin: ['admin.*','users.*'],public: ['home','about','contact'],},},})Every route you expose is visible in the HTML source, so filter out what the client doesn't need. Patterns support * wildcards: admin.* matches admin.login, admin.users.index, and so on.
Note: Filtering is not a security measure. Routes that shouldn't be publicly reachable must be protected by authentication, whether they appear in the client list or not.
import{route}from'@izzyjs/route/client'// Route without parametersroute('users.index').path// "/users"// Route with parametersroute('users.show',{params: {id: '1'}}).path// "/users/1"TypeScript enforces the parameters for you:
route('users.show',{params: {id: '123'}})// ✅route('users.show')// ❌ compile error: missing required param `id`route('users.show',{params: {slug: 'x'}})// ❌ compile error: unknown paramThe second argument accepts params, qs (query string), prefix, and hash:
consturl=route('users.show',{params: {id: '1'},qs: {page: '2'},prefix: '/api/v1',hash: 'profile',})url.path// "/api/v1/users/1?page=2#profile"url.url// "https://example.com/api/v1/users/1?page=2#profile"url.method// "get"url.pattern// "/users/:id"url.name// "users.show"url.qs// URLSearchParamsurl.hash// "profile"The returned object extends String, so you can use it directly wherever a string is expected (e.g. href in Inertia's <Link>).
Optional parameters (:slug? in your AdonisJS route) can simply be omitted:
// router.get('/posts/:id/:slug?', ...).as('posts.show')route('posts.show',{params: {id: '123',slug: 'my-post'}}).path// "/posts/123/my-post"route('posts.show',{params: {id: '123'}}).path// "/posts/123"// router.get('/posts/:category?', ...).as('posts.index')route('posts.index',{params: {category: 'tech'}}).path// "/posts/tech"route('posts.index').path// "/posts"path is always relative; url includes protocol and host, built from baseUrl:
constuser=route('users.show',{params: {id: '123'}})user.path// "/users/123"user.url// "https://example.com/users/123"When a route is registered under a specific domain (AdonisJS router.group().domain(...)), url uses that domain instead of the baseUrl host — the protocol and port still come from baseUrl:
route('home').url// "https://example.com/" (domain: root)route('api.users.index').url// "https://api.example.com/users" (domain: api.example.com)If baseUrl is invalid, url falls back to the relative path.
route() with no arguments returns a helper for inspecting the current request:
route().current()// "/users/1" — the current pathroute().current('users.show',{id: '1'})// true/false — match by name + paramsroute().current('users.*')// true/false — wildcard matchroute().current('/users/*')// true/false — wildcard on the pathMatching by name is domain-aware. If admin.login and user.login both resolve to /login on different subdomains, route().current('admin.login') is only true when you're actually on the admin subdomain:
// On admin.example.com/loginroute().current('admin.login')// trueroute().current('user.login')// falseroute().has('users.show')// true — the named route existsroute().has('users.*')// true — at least one route matchesroute().params// { id: '1' } — params extracted from the current URLGroups defined in your config are exported alongside the generated routes:
import{routes,groups}from'@izzyjs/route/routes'routes// every exposed routegroups.admin// only the routes matching the `admin` group patternsgroups.public// only the `public` groupHeads up:
routesandgroupslive in@izzyjs/route/routes(the generated file), not@izzyjs/route/client. Thegroupsexport always exists — it's an empty object when no groups are configured.
builder combines route resolution with a fetch-based HTTP client, so you can call your own API without hardcoding URLs:
importbuilderfrom'@izzyjs/route/builder'constresult=awaitbuilder('users.show',{id: '123'}).withQs({include: 'profile'}).request().successType<User>().failedType<ApiError>().run()if(result.data){console.log(result.data)// typed as User}else{console.log(result.error)// typed as ApiError}run() never throws — it always resolves to { data, error } where exactly one is set.
Sending data (POST/PUT/PATCH):
constresult=awaitbuilder('users.store').request().withData({name: 'John Doe',email: 'john@example.com'}).successType<User>().run()Builder methods:
| Method | Purpose |
|---|---|
withQs(qs) | Add query string parameters |
withHash(hash) | Add a hash fragment |
withPrefix(prefix) | Prepend a path prefix |
route() | Return the resolved Route (no request made) |
request(config?) | Create the request (headers, timeout, etc.) |
withData(data) | Set the request body |
successType<T>() | Type the success payload |
failedType<T>() | Type the error payload |
run() | Execute and resolve to { data, error } |
The HTTP client automatically:
- sends the
XSRF-TOKENcookie back as theX-XSRF-TOKENheader (CSRF protection) - detects
Content-Typefrom the body (JSON,FormData,File,Blob,ArrayBuffer, plain text) - times out after 30s (configurable via
request({ timeout }))
// Per-request configurationawaitbuilder('users.show',{id: '123'}).request({headers: {Authorization: 'Bearer token'},credentials: 'include',}).run()The configure step registers a Japa plugin that loads your named routes into the test environment, so route() works inside your tests without a running server.
Contributions are welcome — see the Contribution Guidelines and Code of Conduct.
MIT License © IzzyJs