How to mount a router on a service
View SourceA router sends each request to a handler by method and path, extracts
path parameters for you, and answers 404 and 405 on its own. You need it
once your service has more than one endpoint and you would rather list
routes than grow one large handler with a case on the path.
Compile and mount a router
Compile a router and pass it to start_service/1 as router:
Router = livery_router:compile([
{<<"GET">>, <<"/">>, {my_app, index}},
{<<"GET">>, <<"/users/:id">>, {my_app, show}},
{<<"POST">>, <<"/users">>, {my_app, create}}
]),
{ok, Pid} = livery:start_service(#{
http => #{port => 8080},
middleware => [{livery_request_id, undefined},
{livery_access_log, #{}}],
router => Router
}).Each route handler is a normal handler, fun(Req) -> Resp or
{Module, Function}, and receives the request with path parameters
already bound:
show(Req) ->
Id = livery_req:binding(<<"id">>, Req),
livery_resp:json(200, lookup(Id)).start_service/1 takes exactly one of router or handler. Use
handler for one handler that takes every request; use router to pick
a handler per route. The service-level middleware stack wraps every
route.
What you get for free
- 404 for an unmatched path.
- 405 with an
Allowheader for a known path on the wrong method. - Path bindings on
livery_req:bindings/1/binding/2,3.
Add per-route middleware
A route's optional Meta map (the fourth tuple element) may carry a
middleware stack that runs only for that route, inside any
service-level stack:
Auth = {my_auth, #{required => true}},
Router = livery_router:compile([
{<<"GET">>, <<"/public">>, {my_app, public}},
{<<"GET">>, <<"/private">>, {my_app, private}, #{middleware => [Auth]}}
])./private runs Auth before its handler; /public does not. The order
is: service stack (outermost), then route matching, then the route
stack, then the handler.
Compose routers
Once a part of your app stands on its own, it is easier to keep its routes together and add them as a block than to copy them into one big list. Three functions do this, and each returns a router you can combine again.
merge/2 puts two routers side by side. The later one wins if they
register the same method and path:
App = livery_router:compile([{<<"GET">>, <<"/">>, {my_app, index}}]),
Router = livery_router:merge(App, livery_mcp:router()).nest/3 mounts a sub-router under a prefix. Here the MCP endpoint, which
lives at /mcp on its own, ends up at /ai/mcp:
Router = livery_router:nest(<<"/ai">>, livery_mcp:router(), App).layer/2 wraps a whole router in a middleware stack, which is the easy
way to apply one rule, say auth, to every route under a prefix:
Admin = livery_router:compile([{<<"GET">>, <<"/admin/stats">>, {my_app, stats}}]),
Guarded = livery_router:nest(<<"/v1">>, livery_router:layer([Auth], Admin), App).Need the flat list back, for example to pass it to
livery_openapi:build/1? livery_router:routes/1 rebuilds it from any
router, combined or not.
Customise 404 / 405
To control the fallbacks, build the handler yourself with
livery:router_handler/2 and pass it as handler:
H = livery:router_handler(Router, #{
not_found => fun(_R) -> livery_resp:json(404, problem()) end,
method_not_allowed => fun(_R, _Methods) -> livery_resp:empty(405) end
}),
livery:start_service(#{http => #{port => 8080}, handler => H}).Use a router without the service
livery:router_handler/1 returns a plain handler fun, so you can also
use it with a single listener or call it directly in tests:
H = livery:router_handler(Router),
Cap = livery_test_adapter:run([], H,
#{method => <<"GET">>, path => <<"/users/42">>}).See also
- Concept: Routing
- Tutorial: Your first service
- Reference:
livery_router,livery