Cask Scala HTTP Micro-framework
repository·master·Indexed 20 days ago
https://github.com/com-lihaoyi/caskA simple Scala HTTP micro-framework inspired by Python's Flask, designed for building websites, backend servers, and REST APIs. Cask features a lightweight routing system with support for Java 21 Virtual Threads, JSON and form-encoded POST data via decorators like @postJson and @postForm, and flexible request/response handling. It provides built-in support for static files, cookies, and custom error handling through the Main and MainRoutes classes.
What's inside Cask
How Cask defines HTTP endpoints using functions
masterCask uses a "Functions First" approach inspired by Flask. Instead of using complex DSLs or custom action types, you define HTTP endpoints using standard Scala
deffunctions.Function parameters are used to define the endpoint's requirements:
- The parameters the endpoint takes.
- Whether parameters are optional (via default values).
- The return type (e.g., returning a
Response).
Cask uses annotations to extend these functions with HTTP-specific metadata, such as the request path, automated deserialization (from JSON, Form-encoded, or Query-string), and custom logic like logging or authentication.
Create custom `cask.Endpoint`s for advanced control
masterWhen standard decorators like
@cask.getor@cask.postJsonare insufficient, you can define customcask.Endpoints. This allows you to:- Customize Return Types: Change how the annotated function's return type is processed (e.g., automatically serializing objects to JSON via uPickle, Circe, or Jackson, or to bytes via Protobufs).
- Control Parameter Sources: Define where the first parameter list is extracted from (e.g., request headers, a specific part of a protobuf body, or custom query params).
- Customize Deserialization: Swap the default JSON library (uPickle) for others like Circe or Jackson.
- DRY up Decorators: Group common sets of decorators into a single custom endpoint to separate business logic from plumbing.
Use custom endpoints when you need to enforce a standard way of handling requests/responses across your application or when integrating with specific serialization protocols.
How Cask annotations work and how to extend them
masterCask's annotations are not just magic markers; they are self-contained classes that contain all the logic required for them to function. This makes them behave more like Python decorators than traditional Java/Scala annotations.
Key benefits include:
- Inspectability: You can jump to the definition of an annotation to see its logic.
- Extensibility: You can implement your own annotations by creating decorators or custom endpoints.
- Composability: Stacking multiple annotations on a single function follows a well-defined contract and semantics.
Access low-level Undertow handlers
masterCask is built on the Undertow web server. If you require low-level webserver customization not exposed by the standard Cask API, you can overridedefaultHandlerto implement or use Undertow's own handler API.Cask design philosophy and concurrency model
masterCask is designed for simplicity and targets the "99% of code" where simple, synchronous logic is sufficient.
Unlike enterprise frameworks, Cask intentionally avoids built-in support for:
- Async
- Akka
- Streaming Computations
- Backpressure
Endpoints are synchronous by default and do not tie you to a specific concurrency model. For specialized needs, Cask provides a low-level websockets API that allows you to wrap it with your own concurrency library of choice.
Accessing low-level Undertow APIs in Cask
masterCask is a thin wrapper around the Undertow HTTP server. If you need functionality beyond Cask's routing and endpoint system, you can drop down to the lower-level Undertow APIs in several ways:
- Request Access: Request the
exchange: HttpServerExchangedirectly in your endpoint function parameters. - Custom Handlers: Override
defaultHandlerto add your own Undertow handlers alongside Cask's. - Server Initialization: Override
mainto change how the server is initialized.
- Request Access: Request the
How decorators work in Cask
masterDecorators allow you to extend endpoints with additional logic like authentication, rate-limiting, or request-scoped resources (e.g., database transactions).
To create a decorator, implement the
cask.Decoratorinterface and itsgetRawParamsfunction:getRawParamsreceives acask.Request.- It returns
Either[Response, cask.Decor[Any]].Left(response): Bails out early with the provided response (e.g., 403 Forbidden).Right(decor): Provides a map of parameters to be passed to the endpoint and an optional cleanup function.
Parameter Order: Each decorator adds a new parameter list to the right of the existing ones in the endpoint function.
Global Application:
- Apply to all routes in a
cask.Routesobject via thedecoratorsfield. - Apply to every endpoint in the entire application via
cask.Main#mainDecorators.
// Example of a decorator adding a 'User' object to the endpoint class AuthDecorator extends cask.Decorator { override def getRawParams(request: cask.Request): Either[cask.Response, cask.Decor[Any]] = { if (request.headers.contains("Authorization")) { Right(cask.Decor(Map("user" -> "some_user"), cleanup = () => { println("Cleaning up user session") })) } else { Left(cask.Response(403)) } } } // Usage in an endpoint @cask.get("/") @AuthDecorator def protectedRoute(user: String): String = s"Hello $user"Run Cask with Java 21 Virtual Threads
masterCask supports Java 21/Loom Virtual Threads. This can be used to improve concurrency handling. Refer to the official documentation for specific configuration steps on running with virtual threads.Create a minimal Cask application
masterTo create a basic Cask application, define an object that inherits from
cask.MainRoutes. Use annotations like@cask.getor@cask.postto define endpoints.Endpoints can:
- Return raw data (which Cask converts to a response).
- Return a
cask.Responsefor custom status codes or headers. - Accept an optional
cask.Requestto access the full incoming HTTP request.
As your application grows, you can separate routing logic from the main entry point by using
cask.Routesfor routing andcask.Mainfor configuration (like port or host).object MyRoutes extends cask.MainRoutes { @cask.get("/") def hello(): String = "Hello World!" @cask.post("/do-thing") def doThing(request: cask.Request): String = { request.body().toString.reverse } } object Main extends cask.Main(MyRoutes)Install Cask in a Scala project
masterYou can add Cask to your existing Scala project using the following dependency coordinates depending on your build tool:
Mill
// Mill mvn"com.lihaoyi::cask:0.11.3"SBT
// SBT "com.lihaoyi" %% "cask" % "0.11.3"// Mill mvn"com.lihaoyi::cask:0.11.3" // SBT "com.lihaoyi" %% "cask" % "0.11.3"Enable Gzip & Deflate response compression
masterCask provides the
@cask.decorators.compressdecorator to gzip or deflate response bodies. This is useful if you do not have a reverse proxy (like Nginx) handling compression.You can apply this decorator at two levels:
- Route Level: Apply it to a specific set of
cask.Routes. - Global Level: Apply it within your
cask.Mainobject to affect all routes.
- Route Level: Apply it to a specific set of