Cask Scala HTTP Micro-framework

repository·master·Indexed 20 days ago

https://github.com/com-lihaoyi/cask

A 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.

Tokens
6.5K
Snippets
18
Records
39
Agent score
70%

What's inside Cask

  1. How Cask defines HTTP endpoints using functions

    master

    Cask uses a "Functions First" approach inspired by Flask. Instead of using complex DSLs or custom action types, you define HTTP endpoints using standard Scala def functions.

    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.

  2. Create custom `cask.Endpoint`s for advanced control

    master

    When standard decorators like @cask.get or @cask.postJson are insufficient, you can define custom cask.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.

  3. How Cask annotations work and how to extend them

    master

    Cask'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.
  4. Cask design philosophy and concurrency model

    master

    Cask 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.

  5. Accessing low-level Undertow APIs in Cask

    master

    Cask 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:

    1. Request Access: Request the exchange: HttpServerExchange directly in your endpoint function parameters.
    2. Custom Handlers: Override defaultHandler to add your own Undertow handlers alongside Cask's.
    3. Server Initialization: Override main to change how the server is initialized.
  6. How decorators work in Cask

    master

    Decorators 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.Decorator interface and its getRawParams function:

    1. getRawParams receives a cask.Request.
    2. 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.Routes object via the decorators field.
    • 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"
  7. Run Cask with Java 21 Virtual Threads

    master
    Cask 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.
  8. Create a minimal Cask application

    master

    To create a basic Cask application, define an object that inherits from cask.MainRoutes. Use annotations like @cask.get or @cask.post to define endpoints.

    Endpoints can:

    • Return raw data (which Cask converts to a response).
    • Return a cask.Response for custom status codes or headers.
    • Accept an optional cask.Request to access the full incoming HTTP request.

    As your application grows, you can separate routing logic from the main entry point by using cask.Routes for routing and cask.Main for 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)
  9. Enable Gzip & Deflate response compression

    master

    Cask provides the @cask.decorators.compress decorator 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:

    1. Route Level: Apply it to a specific set of cask.Routes.
    2. Global Level: Apply it within your cask.Main object to affect all routes.