Router
The router framework extracts endpoint definitions from controllers at compile-time and matches incoming requests to handler methods.
Router Builder
Creating a Router
Use Router.of[T] to extract routes from a controller class:
import wvlet.uni.http.HttpMethod
import wvlet.uni.http.router.{Endpoint, Router}
class UserController:
@Endpoint(HttpMethod.GET, "/users")
def listUsers(): String = "[]"
@Endpoint(HttpMethod.GET, "/users/:id")
def getUser(id: String): String = s"""{"id":"${id}"}"""
val router = Router.of[UserController]
// Check extracted routes
println(router.routeSummary)
// Router:
// GET /users -> listUsers
// GET /users/:id -> getUserThe macro extracts all methods annotated with @Endpoint and creates route definitions.
Combining Routers
Use andThen to combine multiple routers:
class UserController:
@Endpoint(HttpMethod.GET, "/users")
def listUsers(): String = "[]"
class ItemController:
@Endpoint(HttpMethod.GET, "/items")
def listItems(): String = "[]"
class HealthController:
@Endpoint(HttpMethod.GET, "/health")
def health(): String = "ok"
val router = Router.of[HealthController]
.andThen(Router.of[UserController])
.andThen(Router.of[ItemController])Routes are matched in the order they are added.
Empty Router
Create an empty router to combine with others:
val baseRouter = Router.empty
val fullRouter = baseRouter
.andThen(Router.of[Controller1])
.andThen(Router.of[Controller2])Controller Provider
The ControllerProvider interface determines how controller instances are created.
SimpleControllerProvider
The default provider creates instances using reflection:
import wvlet.uni.http.router.SimpleControllerProvider
import wvlet.uni.http.netty.RouterHandler
val router = Router.of[UserController]
val handler = RouterHandler(router) // Uses SimpleControllerProvider
// Equivalent to:
val handler2 = RouterHandler(router, SimpleControllerProvider())Controllers must have a no-argument constructor.
MapControllerProvider
For pre-configured instances (useful for testing or dependency injection):
import wvlet.uni.http.router.MapControllerProvider
class UserController(userService: UserService):
@Endpoint(HttpMethod.GET, "/users")
def listUsers(): Seq[User] = userService.findAll()
// Create controller with dependencies
val userService = UserService()
val controller = UserController(userService)
// Register pre-created instance
val provider = MapControllerProvider(controller)
val handler = RouterHandler(router, provider)Multiple controllers:
val provider = MapControllerProvider(
UserController(userService),
ItemController(itemService),
HealthController()
)Custom Provider
Implement ControllerProvider for custom instantiation logic:
import wvlet.uni.http.router.ControllerProvider
import wvlet.uni.surface.Surface
class DIControllerProvider(container: DIContainer) extends ControllerProvider:
override def get(surface: Surface): Any =
container.getInstance(surface.rawType)
override def getFilter(surface: Surface): Any =
container.getInstance(surface.rawType)Route Matching
Match Order
Routes are matched sequentially in the order they were added:
val router = Router.of[SpecificController] // Matched first
.andThen(Router.of[GeneralController]) // Matched secondMore specific routes should be added before general ones.
Path Matching
Routes match when:
- HTTP method matches exactly
- Number of path segments matches
- Each segment matches (literal or parameter)
// Route: GET /users/:id
// Matches: GET /users/123 -> id = "123"
// Matches: GET /users/alice -> id = "alice"
// No match: GET /users/123/posts (different segment count)
// No match: POST /users/123 (different method)Path Components
Paths are parsed into components:
| Pattern | Component Type | Matches |
|---|---|---|
users | Literal | Exact string "users" |
:id | Parameter | Any string (captured) |
// GET /users/:userId/posts/:postId
// Matches: GET /users/1/posts/42
// Extracted: userId = "1", postId = "42"Router with Filters
Add filters that apply to all routes in a router:
import wvlet.uni.http.netty.RxHttpFilter
class AuthFilter extends RxHttpFilter:
def apply(request: Request, next: RxHttpHandler): Rx[Response] =
if isAuthenticated(request) then
next.handle(request)
else
Rx.single(Response.unauthorized)
val router = Router
.filter[AuthFilter]
.andThen(Router.of[ProtectedController])The filter is instantiated using the same ControllerProvider as controllers.
RouterHandler
The RouterHandler connects a router to the Netty server:
import wvlet.uni.http.netty.{RouterHandler, NettyServer}
val router = Router.of[MyController]
val handler = RouterHandler(router)
NettyServer
.withPort(8080)
.withRxHandler(handler)
.start()With Custom Provider
val handler = RouterHandler(router, myControllerProvider)With Pre-registered Controllers
val handler = RouterHandler.withControllers(
router,
controller1,
controller2
)Request Mapping
The HttpRequestMapper binds request data to method parameters in this order:
- Path parameters - From URL path (
:id-> value) - Query parameters - From URL query string
- Default values - Method parameter defaults
- Request object - If parameter type is
Request
@Endpoint(HttpMethod.GET, "/search/:category")
def search(
category: String, // From path: /search/books
query: String, // From query: ?query=scala
limit: Int = 10, // From query or default
request: Request // The full request
): Response = ???Request: GET /search/books?query=scala&limit=20
category= "books"query= "scala"limit= 20request= full Request object
