Handlers & Results
Every request processor in Apion — routes, middleware, and error handlers — uses a single type:
type Handler = Request => Future[Result]Result Types
Section titled “Result Types”A handler returns one of four results:
sealed trait Result
case class Continue(request: Request) extends Resultcase class Complete(response: Response) extends Resultcase class Fail(error: ServerError) extends Resultcase object Skip extends ResultContinue
Section titled “Continue”Pass a (possibly modified) request to the next handler. This is how middleware works — it transforms the request and passes it along:
val addTimestamp: Handler = request => Future.successful(Continue( request.copy(context = request.context + ("startTime" -> System.currentTimeMillis())) ))Complete
Section titled “Complete”End the handler chain and send a response to the client:
val hello: Handler = _ => Future.successful(Complete(Response.text("Hello World")))
// Or using the DSL:val hello: Handler = _ => "Hello World".asTextPropagate a typed error. The error flows to the next registered error handler:
val requireId: Handler = request => request.params.get("id") match { case Some(id) => Future.successful(Continue(request)) case None => failValidation("Missing id parameter") }Skip this handler and try the next one. Useful in error handlers that only handle specific error types:
server.use { (error: ServerError, request: Request) => error match { case e: ValidationError => Map("error" -> e.message).asJson(400) case _ => skip // Let the next error handler deal with it }}Request Flow
Section titled “Request Flow”- The server receives an HTTP request and creates an immutable
Request - Handlers execute sequentially (middleware first, then routes)
- Each handler returns a
Resultthat determines what happens next - When a handler returns
Complete, response finalizers run in LIFO order - The response is sent to the client
Error Types
Section titled “Error Types”Apion provides three built-in error types:
trait ServerError extends Throwable
case class ValidationError(message: String) extends ServerErrorcase class AuthError(message: String) extends ServerErrorcase class NotFoundError(message: String) extends ServerErrorCreate errors using the DSL helpers:
failValidation("Invalid input") // Future[Fail(ValidationError(...))]failAuth("Unauthorized") // Future[Fail(AuthError(...))]failNotFound("Not found") // Future[Fail(NotFoundError(...))]fail(CustomError("oops")) // Future[Fail(yourError)]Custom Error Types
Section titled “Custom Error Types”Define domain-specific errors by extending ServerError:
case class RateLimitError(message: String, retryAfter: Long) extends ServerErrorcase class ConflictError(message: String) extends ServerErrorError Handlers
Section titled “Error Handlers”Error handlers receive both the error and the original request:
server.use { (error: ServerError, request: Request) => error match { case ValidationError(msg) => Map("error" -> "validation", "message" -> msg).asJson(400) case AuthError(msg) => Map("error" -> "auth", "message" -> msg).asJson(401) case NotFoundError(msg) => Map("error" -> "not_found", "message" -> msg).asJson(404) case _ => Map("error" -> "internal").asJson(500) }}Error handlers can also transform errors:
server.use { (error: ServerError, _: Request) => error match { case e: ValidationError => Future.successful(Fail(CustomError(s"Validation failed: ${e.message}"))) case _ => skip }}Composing Handlers
Section titled “Composing Handlers”Route-Level Chaining
Section titled “Route-Level Chaining”Pass multiple handlers to a route — they execute sequentially:
server.get("/admin/users", authMiddleware, // Verify JWT requireAdmin, // Check admin role listUsersHandler // Return data)Global Middleware
Section titled “Global Middleware”Apply a handler to all routes:
server .use(LoggingMiddleware()) .use(CorsMiddleware())Path-Scoped Middleware
Section titled “Path-Scoped Middleware”Apply a handler only to routes matching a prefix:
server.use("/api", authMiddleware)Finalizers
Section titled “Finalizers”Finalizers transform a response after a handler returns Complete. They execute in LIFO (last-in, first-out) order:
type Finalizer = (Request, Response) => Future[Response]Middleware adds finalizers via request.addFinalizer:
val timing: Handler = request => { val start = System.currentTimeMillis()
val finalizer: Finalizer = (_, response) => { val duration = System.currentTimeMillis() - start Future.successful(response.copy( headers = response.headers.add("X-Response-Time", s"${duration}ms") )) }
Future.successful(Continue(request.addFinalizer(finalizer)))}This pattern is used by CompressionMiddleware, LoggingMiddleware, and CookieMiddleware to inspect or modify responses without breaking the linear handler flow.