12 Router
We’ve talked about routing. To come full circle, let’s take a look at routers in Ambiorix.
12.1 What is a router?
Structure and maintainability are key when building applications. As an application grows larger and larger, we need a way to organize the routes.
Enter routers.
A Router object is an instance containing middleware and routes. Think of it as a “mini-application”, only capable of performing middleware and routing functions.
A router behaves like middleware itself, therefore you can use it:
- as an argument to
app$use(), or - as the argument to another router’s
use()method.
Once you’ve created a Router object, you can add middleware and HTTP method routes (get, post, put, delete, etc) to it just like an application.
12.2 Why routers matter
Routers start to make sense the moment an application grows beyond a handful of routes.
Consider a very common feature: users.
At first, the routes are simple:
GET /users
GET /users/:id
POST /users
PUT /users/:id
DELETE /users/:idIf everything lives directly on the app instance, these routes sit alongside unrelated concerns:
/
/login
/logout
/health
/users
/users/:id
/posts
/posts/:idThis works, but only briefly.
As the /users feature evolves, you inevitably add shared behavior:
- authentication
- authorization
- validation
- feature-specific helpers
Without routers, you either repeat middleware on every route, or rely on ordering tricks and comments to keep things readable. The application still works, but the structure starts to decay.
At this point, routers become a necessity.
A router lets you group routes and middleware by responsibility. Everything related to users lives together, and shared behavior is applied once.
library(ambiorix)
# ------------------------------------------------------------------------------
users <- Router$new(path = "/users")
auth <- function(req, res) {
now <- format(
x = Sys.time(),
format = "%F %T",
usetz = TRUE
)
cat("[authentication middleware]", now, "\n")
}
users$use(auth)
users$get("/", function(req, res) {
res$send("list users")
})
users$get("/:id", function(req, res) {
res$send(paste("show user", req$params$id))
})
users$post("/", function(req, res) {
res$send("create user")
})
users$put("/:id", function(req, res) {
res$send(paste("update user", req$params$id))
})
users$delete("/:id", function(req, res) {
res$send(paste("delete user", req$params$id))
})
# ------------------------------------------------------------------------------
app <- Ambiorix$new()
app$get("/", function(req, res) {
res$send("hello, routers!")
})
app$use(users)
app$start(port = 3000L)In this new structure:
- user-related routes are defined in one place.
- shared middleware is applied once.
- the main application stays readable as it grows.
That is the role of a router.
12.3 Nested routers
Routers can be mounted inside other routers.
This is useful when a feature has multiple sub-features that should stay separate, but still live under one umbrella.
An admin area is the classic case:
/adminis the parent feature./admin/usersand/admin/reportsare independent sub-features.- the parent can enforce shared behavior (audit logs, auth, etc.)
The key idea is composition:
- a parent router defines a prefix and shared middleware.
- child routers define their own routes.
- mounting the child routers under the parent produces a clean URL tree.
- middleware on the parent applies to everything mounted under it.
In practice, you write routes where they belong, then “paste” them together.
Say this is the hierarchy we need:
- /admin
- /users
- /reports
- /daily
Using nested routers, our app would be:
library(ambiorix)
# ------------------------------------------------------------------------------
admin <- Router$new("/admin")
users <- Router$new("/users")
reports <- Router$new("/reports")
audit <- function(req, res) {
now <- format(
x = Sys.time(),
format = "%F %T",
usetz = TRUE
)
cat("[audit middleware]", now, "\n")
}
admin$use(audit)
admin$get("/", function(req, res) {
res$send("Admin home")
})
users$get("/", function(req, res) {
res$send("Admin users")
})
reports$get("/", function(req, res) {
res$send("Admin reports")
})
reports$get("/daily", function(req, res) {
res$send("Daily report")
})
admin$use(users)
admin$use(reports)
# ------------------------------------------------------------------------------
app <- Ambiorix$new()
app$get("/", function(req, res) {
res$send("hello, nested routers!")
})
app$use(admin)
app$start(port = 3000L)Once composed, the effective routes become:
/admin
/admin/users
/admin/reports
/admin/reports/dailyAnd audit() runs for all of them.
Nested routers let you keep each area small and focused, while still getting a coherent URL hierarchy and shared behavior.