Routing
Defining Routes
Section titled “Defining Routes”Register handlers for specific HTTP methods and paths:
server .get("/users", listUsers) .post("/users", createUser) .put("/users/:id", updateUser) .delete("/users/:id", deleteUser) .patch("/users/:id", patchUser)Path Parameters
Section titled “Path Parameters”Named segments prefixed with : are captured into request.params:
server.get("/users/:id", request => { val userId = request.params("id") s"User: $userId".asText})
// Multiple parametersserver.get("/users/:userId/posts/:postId", request => { val userId = request.params("userId") val postId = request.params("postId") getPost(userId, postId).asJson})Wildcards
Section titled “Wildcards”Use * for catch-all segments:
server.get("/files/*", request => { // Matches /files/foo, /files/foo/bar, etc. StaticMiddleware("uploads")(request)})Multiple Handlers Per Route
Section titled “Multiple Handlers Per Route”Chain handlers for a single route — they execute sequentially:
server.get("/admin/users", authMiddleware, // First: verify authentication requireAdmin, // Second: check admin role listUsersHandler // Third: return data)If any handler returns Complete, Fail, or Skip, the chain stops.
Subrouters
Section titled “Subrouters”Group related routes under a common prefix using Router:
val usersRouter = Router() .get("/", listUsers) // GET /api/users .post("/", createUser) // POST /api/users .get("/:id", getUser) // GET /api/users/:id .put("/:id", updateUser) // PUT /api/users/:id .delete("/:id", deleteUser) // DELETE /api/users/:id
server.use("/api/users", usersRouter)Nested Subrouters
Section titled “Nested Subrouters”Routers can be nested arbitrarily:
val postsRouter = Router() .get("/", listPosts) .post("/", createPost)
val usersRouter = Router() .get("/", listUsers) .use("/:userId/posts", postsRouter) // Nest posts under users
val apiRouter = Router() .use("/users", usersRouter)
server.use("/api", apiRouter)// Resolves: GET /api/users/:userId/postsRouter-Scoped Middleware
Section titled “Router-Scoped Middleware”Middleware added to a router applies only to its routes:
val adminRouter = Router() .use(requireAdmin) // Only applies to admin routes .get("/dashboard", dashboardHandler) .get("/users", adminListUsers)
val publicRouter = Router() .get("/status", statusHandler) // No admin check
server .use("/admin", adminRouter) .use("/public", publicRouter)Path-Scoped Middleware
Section titled “Path-Scoped Middleware”Apply middleware to all routes matching a prefix:
// All /api/* routes require authenticationserver.use("/api", authMiddleware)
// Static files under /assetsserver.use("/assets", StaticMiddleware("public/assets"))Route Matching
Section titled “Route Matching”Routes are matched by splitting paths into segments:
- Static segments are matched literally (
/usersmatches/users) - Parameter segments (
:name) match any single segment - Wildcard (
*) matches remaining segments - Static segments take priority over parameter segments
Route patterns are compiled once at server startup for efficient matching.
Global Middleware
Section titled “Global Middleware”Middleware registered with .use(handler) (no path) applies to all requests:
server .use(LoggingMiddleware()) // Runs on every request .use(CorsMiddleware()) // Runs on every request .get("/hello", helloHandler) // Only matches GET /helloError Handlers
Section titled “Error Handlers”Register error handlers with the two-argument form of .use:
server.use { (error: ServerError, request: Request) => error match { case ValidationError(msg) => Map("error" -> msg).asJson(400) case _ => skip }}Error handlers are tried in order. Return skip to pass to the next one.