What is the coRouter { } DSL in Spring WebFlux and how do coroutine-based functional handlers differ from the reactive router() DSL?
answer
- coRouter = suspend router
- handler: suspend (ServerRequest) -> ServerResponse (not Mono)
- awaitBody / bodyValueAndAwait / bodyAndAwait / buildAndAwait
- same GET/POST/nest predicates
- returns RouterFunction bean
basics
~20 scoRouter { } is the coroutine version of the functional routing DSL. Routes map to suspend handler functions that take a ServerRequest and return a ServerResponse directly (not a Mono<ServerResponse>), so you write sequential coroutine code.
solid answer
~40 sWebFlux offers functional (annotation-free) endpoints via `RouterFunction`s. The reactive builder is `router { }`, where handlers are `(ServerRequest) -> Mono<ServerResponse>`. `coRouter { }` (in `org.springframework.web.reactive.function.server`) is its coroutine counterpart: handlers are `suspend (ServerRequest) -> ServerResponse`, returning the response value directly. You read the body with suspend extensions like `request.awaitBody<T>()` and build responses with suspend builders like `ServerResponse.ok().bodyValueAndAwait(dto)`, `.bodyAndAwait(flow)`, or `.buildAndAwait()`. The route-matching DSL (`GET`, `POST`, `path`, `nest`, `accept`) is identical to `router { }`; only the handler signature and the terminal builders differ. It returns a normal `RouterFunction<ServerResponse>` bean, so it coexists with annotated controllers. Use it when you prefer explicit, testable route wiring and coroutine ergonomics over annotations.
code
kotlin · 28 lines@Bean
fun apiRoutes(handler: OrderHandler): RouterFunction<ServerResponse> = coRouter {
accept(MediaType.APPLICATION_JSON).nest {
GET("/orders/{id}", handler::byId)
POST("/orders", handler::create)
}
GET("/orders/stream", handler::stream)
}
class OrderHandler(private val service: OrderService) {
suspend fun byId(req: ServerRequest): ServerResponse {
val order = service.find(req.pathVariable("id").toLong())
?: return ServerResponse.notFound().buildAndAwait()
return ServerResponse.ok().bodyValueAndAwait(order)
}
suspend fun create(req: ServerRequest): ServerResponse {
val body = req.awaitBody<CreateOrder>()
return ServerResponse.status(HttpStatus.CREATED)
.bodyValueAndAwait(service.create(body))
}
// Flow<T> streaming body
suspend fun stream(req: ServerRequest): ServerResponse =
ServerResponse.ok()
.contentType(MediaType.TEXT_EVENT_STREAM)
.bodyAndAwait(service.streamAll())
}go deeper
Know coRouter is the coroutine version of the functional router DSL with suspend handlers returning ServerResponse.
Fluently use awaitBody/pathVariable and the bodyValueAndAwait/bodyAndAwait/buildAndAwait builders; know the GET/POST/nest predicates carry over.
Explain that coRouter still produces a standard RouterFunction bridged to Reactor, the empty-body throw semantics, and streaming via Flow+bodyAndAwait.
Argue functional vs annotated endpoints for composability/testability, and reason about cross-cutting filters, error handling, and event-loop discipline in a coRouter app.
**Functional endpoints background.** Besides annotation-based `@RestController`s, WebFlux supports *functional* endpoints: you declare routes as data by registering a `RouterFunction<ServerResponse>` bean. Each route couples a `RequestPredicate` (method + path + headers) to a `HandlerFunction`. The reactive DSL is `org.springframework.web.reactive.function.server.router { }`, in which every handler has signature `(ServerRequest) -> Mono<ServerResponse>`. **`coRouter { }`.** This is the coroutine-flavored builder in the same package. Structurally it is the same DSL — `GET("/x")`, `POST("/y")`, `path`, `nest`, `accept`, `contentType`, filters — but the handler functions are **`suspend (ServerRequest) -> ServerResponse`**. Two differences matter: 1. The handler returns a bare `ServerResponse`, not `Mono<ServerResponse>`. 2. Handlers are `suspend`, so inside them you call other suspend functions directly. **Suspend request/response extensions.** Because the handler is suspending, you use the `await`/`AndAwait` extension family instead of Reactor operators: - Read body: `request.awaitBody<Foo>()`, `request.awaitBodyOrNull<Foo>()`, `request.bodyToFlow<Foo>()` (streaming), `request.awaitFormData()`, `request.awaitMultipartData()`. - Path/query: `request.pathVariable("id")`, `request.queryParamOrNull("q")`. - Build response: `ServerResponse.ok().bodyValueAndAwait(value)`, `ServerResponse.ok().bodyAndAwait(flow)` (for a `Flow<T>` streaming body), `ServerResponse.status(HttpStatus.CREATED).buildAndAwait()`, `ServerResponse.ok().json().bodyValueAndAwait(dto)`. **Full example:** ```kotlin @Configuration class UserRoutes { @Bean fun routes(h: UserHandler) = coRouter { "/api/users".nest { GET("/{id}", h::get) GET("", h::list) POST("", h::create) } } } class UserHandler(private val service: UserService) { suspend fun get(req: ServerRequest): ServerResponse { val id = req.pathVariable("id").toLong() val user = service.load(id) ?: return ServerResponse.notFound().buildAndAwait() return ServerResponse.ok().bodyValueAndAwait(user) } suspend fun create(req: ServerRequest): ServerResponse { val dto = req.awaitBody<CreateReq>() val saved = service.create(dto) return ServerResponse.status(HttpStatus.CREATED).bodyValueAndAwait(saved) } fun list(req: ServerRequest): ServerResponse = TODO() } ``` **Streaming.** For a many-valued body use `bodyAndAwait(flow)`: ```kotlin suspend fun stream(req: ServerRequest): ServerResponse = ServerResponse.ok().contentType(MediaType.TEXT_EVENT_STREAM).bodyAndAwait(service.streamAll()) ``` **How it works.** `coRouter` builds the same `RouterFunction`; each coroutine handler is wrapped so its `suspend` invocation is turned into a `Mono<ServerResponse>` (via the coroutine-to-Reactor bridge from `kotlinx-coroutines-reactor`). So downstream, the infrastructure still sees standard Reactor types. **Gotchas.** - Same event-loop blocking hazard as annotated handlers — do not block; use `withContext(Dispatchers.IO)`. - `awaitBody<T>()` on an empty body throws; use `awaitBodyOrNull<T>()` when the body may be absent. - Handlers may be `suspend` or plain, but to use `await*`/`*AndAwait` you need a `suspend` context. - Requires `kotlinx-coroutines-reactor`. **When to use.** Choose functional/`coRouter` endpoints when you want routing decoupled from handler classes, easy composition/nesting, straightforward unit testing of handlers, or a more explicit style. Choose annotated controllers for convention-driven simplicity. Both can coexist in one app.
- How do you return a streaming (many-valued) body from a coRouter handler?Produce a `Flow<T>` and terminate with `ServerResponse.ok().bodyAndAwait(flow)` (often with `contentType(MediaType.TEXT_EVENT_STREAM)`). `bodyValueAndAwait` is for a single value; `bodyAndAwait` takes the `Flow`/publisher.
- What is the difference between awaitBody<T>() and awaitBodyOrNull<T>()?`awaitBody<T>()` suspends and returns the deserialized body, throwing if the body is empty/absent. `awaitBodyOrNull<T>()` returns `null` instead of throwing when there is no body, which you handle to return a 400/appropriate response.
saying these in an interview costs you the question
- Saying coRouter handlers must return Mono<ServerResponse>.
- Using .body()/.syncBody() Reactor builders instead of the *AndAwait suspend builders.
- Thinking coRouter is a different routing engine rather than the same DSL with suspend handlers.
- Forgetting bodyAndAwait for streaming vs bodyValueAndAwait for a single value.