Use module-level final routes = [...] on a @:router declaration for new code.
Router declarations support:
- Typed tree nodes from
RouterDsl.*static imports (recommended) - Flat route objects
{name, method, path, ...}(compatibility mode)
@:route(...) on controller functions is still supported for legacy/manual routing.
Why this section exists: Phoenix routers are nested (pipeline, scope, live_session). Static-imported router node builders let your Haxe source express that structure directly.
import reflaxe.elixir.macros.RouterDsl.*;
typedef UserPathParams = {
var id:Int;
}
class UsersLive {
public static function index():String return "ok";
}
class UserController {
public static function show():String return "ok";
}
@:native("MyAppWeb.Router")
@:router
final routes = [
pipeline(browser, [
plug(accepts, {initArgs: ["html"]}),
plug(fetch_session)
]),
scope("/", [
pipeThrough([browser]),
liveSession("default", [
live("/", UsersLive),
get("/users/:id", UserController, UserController.show, {
paramsContract: UserPathParams
})
])
])
];defmodule MyAppWeb.Router do
use Phoenix.Router
import Phoenix.LiveView.Router
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
end
scope "/" do
pipe_through :browser
live_session :default do
live "/", UsersLive
get "/users/:id", UserController, :show
end
end
endWhy the compiler emits this: it lowers each router node to the matching Phoenix router macro (pipeline, scope, live_session, get, live, etc.) while preserving nesting from the Haxe source.
For typed router nodes, the compiler validates:
- Controller/live module references are real types.
- Action references exist on the target module.
- Routes with path params (
:id,*rest) includeparamsContract. paramsContractcontains fields for those path params (snake_case-normalized).- Controller/live/action refs should resolve as types (for example
UserControllervia import, orMain.UserControllerin single-file test modules).
Example of a rejected route:
get("/users/:id", UserController, UserController.show)This fails because paramsContract is required when path params are present.
Why this section exists: pipeline and common plug names are a finite set in most apps, so plain strings are easy to mistype and hard to autocomplete.
Preferred typed style:
import reflaxe.elixir.macros.RouterDsl.*;
final routes = [
pipeline(browser, [
plug(accepts, {initArgs: ["html"]}),
plug(fetch_session)
]),
scope("/", [
pipeThrough([browser])
])
];Custom names (still typed):
import reflaxe.elixir.macros.RouterDsl.*;
pipeline(pipelineName("admin"), [
plug(plugName("my_custom_plug"))
]);Explicit escape hatches for legacy/dynamic values:
pipelineUnsafe("admin", [
plugUnsafe("my_custom_plug")
]);
pipeThroughUnsafe(["admin"]);Use *Unsafe only when values are intentionally dynamic or migration glue.
- Structure:
pipeline(name, children)pipelineUnsafe(name, children)(escape hatch)scope(path, children, ?opts)pipeThrough(pipelines)pipeThroughUnsafe(pipelines)(escape hatch)liveSession(name, children, ?opts)plug(target, ?opts)wheretargetis a typed plug token or module referenceplugUnsafe(target, ?opts)(escape hatch)
- Routes:
get/post/put/patch/delete/options/head/connect/trace(path, controller, action, ?opts)live(path, liveModule, ?action, ?opts)match(verb, path, controller, action, ?opts)
- Other Phoenix router macros:
forward(path, moduleRef, ?opts)resources(path, controller, ?opts)resource(path, controller, ?opts)liveDashboard(path, ?opts)mailbox(path, ?opts)
live("/", AppLive)is valid and compiles tolive "/", AppLive.- Add an explicit action only when you need it:
live("/todos/:id/edit", TodoLive, TodoLive.edit)
- WHY: Phoenix supports live routes without
:action; forcing placeholder action methods adds noise.
- Typed route-node options are Phoenix-shaped:
asNamemaps to Phoenixas:paramsContractis a compiler-only type check for path params
- Leave
asNameunset to keep Phoenix default helper-name inference. nameis for flat compatibility-route metadata and is not a Phoenix route keyword.
Prefer reflaxe.elixir.macros.HttpMethod:
GETPOSTPUTPATCHDELETEOPTIONSHEADCONNECTTRACEMATCHLIVELIVE_DASHBOARDMAILBOX
@:routes([...]) remains supported for existing routers and migration work.
import controllers.UserController;
import reflaxe.elixir.macros.HttpMethod;
@:router
@:routes([
{
name: "usersIndex",
method: HttpMethod.GET,
path: "/users",
controller: UserController,
action: UserController.index
}
])
class LegacyRouter {}String controller refs in flat @:routes objects are compatibility-only:
- Default: warning
-D router_strict_typed_refs: compile error
Prefer typed refs in new code.
@:route(...) on controller actions is still available for legacy/manual glue.
@:router
class LegacyRouter {
@:route({method: "GET", path: "/users", controller: "controllers.UserController", action: "index"})
public static function usersIndex():String return "/users";
}For new routers, prefer module-level final routes = [...].
docs/04-api-reference/ANNOTATIONS.mddocs/04-api-reference/TYPE_SAFE_CHILD_SPEC.mddocs/02-user-guide/INTEROP_WITH_EXISTING_ELIXIR.mdexamples/09-phoenix-router/README.md