top of page
Writer's pictureAaron Pritzlaff

Simple Contract-First Applications with Cask

Updated: Oct 8

This article introduces a new openapi (also known as "swagger") code generation template (code here) for the scala cask micro-framework.


In addition to simply supporting the cask framework, this article highlights the rationale for some of the design decisions, and provides a brief example.



What is "contract-first" development?


"Contract-First" is a design approach to creating services (often times "micro-services") which favours starting with an API contract. This approach has become popular for REST services, where you use a description of your service (the endpoints, data structures, possible results, etc) to generate both server and client stubs.

The openapi "petstore" service is a common example:

And you can browse the generated samples on github to see what the resulting generated code looks like.


The alternative to "contract-first" is either no contract (just services agreed or documented internally), or else to take a "code-first" approach.


"code-first" uses annotations on code to generate an API contract which is then used to generate client stubs, or to offer a "Swagger UI" for testing your services.

You can find examples of that sort of approach with Java here, with code which looks a bit like this:



Why would I use contract-first?


The appeal of contract-first is to make your API a first-class citizen, which treats the producers and consumers of the API with the same respect and weight.

It turns API design into a discrete exercise done up-front, and helps to guard against changing the API accidentally when coding, perhaps due to some refactor.


As a practical matter, it also helps when an API is kept in a separate repository from the implementations. This is because many project stakeholders will care about API changes and their consequences (data integrity, support/stability, release strategy, etc) but not particularly care about any one implementation detail.


It also makes separating design governance and the resulting approval rules much easier, since different stakeholders may require approval for API changes.


What code generation options do I have?


The 'openapi-generator' project is written in Java itself, and has templates which use the service specification to generate documentation, other schemas, and server and client code stubs for interacting with the service.


I won't get into all the different examples here. To list all available generators you can use the convenient openapitools/openapi-generator-cli docker image:

docker run --rm openapitools/openapi-generator-cli list

Which will produces a complete list.

There are loads! See the list output here...

The following generators are available:


CLIENT generators:

    - ada

    - android

    - apex

    - bash

    - c

    - clojure

    - cpp-qt-client

    - cpp-restsdk

    - cpp-tiny (beta)

    - cpp-tizen

    - cpp-ue4 (beta)

    - crystal (beta)

    - csharp

    - dart

    - dart-dio

    - eiffel

    - elixir

    - elm

    - erlang-client

    - erlang-proper

    - go

    - groovy

    - haskell-http-client

    - java

    - java-helidon-client (beta)

    - java-micronaut-client (beta)

    - javascript

    - javascript-closure-angular (beta)

    - javascript-flowtyped

    - jaxrs-cxf-client

    - jetbrains-http-client (experimental)

    - jmeter

    - julia-client (beta)

    - k6 (beta)

    - kotlin

    - lua (beta)

    - n4js (beta)

    - nim (beta)

    - objc

    - ocaml

    - perl

    - php

    - php-dt (beta)

    - php-nextgen (beta)

    - powershell (beta)

    - python

    - python-pydantic-v1

    - r

    - ruby

    - rust

    - scala-akka

    - scala-gatling

    - scala-pekko

    - scala-sttp

    - scala-sttp4 (beta)

    - scalaz

    - swift-combine

    - swift5

    - swift6 (experimental)

    - typescript (experimental)

    - typescript-angular

    - typescript-aurelia

    - typescript-axios

    - typescript-fetch

    - typescript-inversify

    - typescript-jquery

    - typescript-nestjs (experimental)

    - typescript-node

    - typescript-redux-query

    - typescript-rxjs

    - xojo-client

    - zapier (beta)



SERVER generators:

    - ada-server

    - aspnetcore

    - cpp-pistache-server

    - cpp-qt-qhttpengine-server

    - cpp-restbed-server

    - cpp-restbed-server-deprecated

    - csharp-functions

    - erlang-server

    - fsharp-functions (beta)

    - fsharp-giraffe-server (beta)

    - go-echo-server (beta)

    - go-gin-server

    - go-server

    - graphql-nodejs-express-server

    - haskell

    - haskell-yesod (beta)

    - java-camel

    - java-helidon-server (beta)

    - java-inflector

    - java-micronaut-server (beta)

    - java-microprofile

    - java-msf4j

    - java-pkmst

    - java-play-framework

    - java-undertow-server

    - java-vertx-web (beta)

    - java-wiremock (beta)

    - jaxrs-cxf

    - jaxrs-cxf-cdi

    - jaxrs-cxf-extended

    - jaxrs-jersey

    - jaxrs-resteasy

    - jaxrs-resteasy-eap

    - jaxrs-spec

    - julia-server (beta)

    - kotlin-server

    - kotlin-spring

    - kotlin-vertx (beta)

    - kotlin-wiremock (beta)

    - nodejs-express-server (beta)

    - php-flight (experimental)

    - php-laravel

    - php-lumen

    - php-mezzio-ph

    - php-slim4

    - php-symfony

    - python-aiohttp

    - python-blueplanet

    - python-fastapi (beta)

    - python-flask

    - ruby-on-rails

    - ruby-sinatra

    - rust-axum (beta)

    - rust-server

    - scala-akka-http-server (beta)

    - scala-cask

    - scala-finch

    - scala-http4s-server

    - scala-lagom-server

    - scala-play-server

    - scalatra

    - spring



DOCUMENTATION generators:

    - asciidoc

    - cwiki

    - dynamic-html

    - html

    - html2

    - markdown (beta)

    - openapi

    - openapi-yaml

    - plantuml (beta)



SCHEMA generators:

    - avro-schema (beta)

    - graphql-schema

    - ktorm-schema (beta)

    - mysql-schema

    - postman-collection (beta)

    - protobuf-schema (beta)

    - wsdl-schema (beta)



CONFIG generators:

    - apache2


Why create a new code generator?


When I started, there wasn't a Scala Cask generator available.


'Cask' is a micro-framework written by Li Haoyi and based on python's 'flask' framework. I found myself agreeing with much of Li Haoyi's rationale for his approach to scala:

it should be intuitive, easy to pick up, pragmatic and fun.

In additional to simply supporting the Cask framework, I also wanted to address many of the other issues I found when working with the other offerings:


  • The Java ones were just a non-starter. Most are based on Spring, so you bind yourself to all the headaches and problems that brings (that deserves another blog). The immediate issue was that you seem to have to copy / repeat a lot of generated code, and can't just jar-up the boilerplate (spring-boot isn't designed to be provided as a library, which its custom class-loaders, etc)

  • I've not found a template which didn't require you to (re-)generate your code in the same project, typically by making the 'compile' step depend on a 'generate' step. I'd prefer to just bring in the generated code as a simple library dependency, as it dramatically simplifies my build and allows me to easily leverage tools like scala-cli, where you can eliminate a build file entirely. With the scala-cask template, you can of-course follow the traditional "generated in the same project" approach, but you can also just jar up all that boilerplate, which allows you to just pull it in as a single dependency and isolate your business logic:


  • I wanted the resulting code to support both composition and inheritance approaches. Many templates require you to extend particular classes, rather than support a more compositional approach. I wanted there to be plenty of hooks which allow you to write your application the way you want.

  • I also took care to address a few other niggles:

    • Generators should provide an example as part of the generated output to help you bootstrap your new service. The cask generator provides an 'example' output directory which you can copy:

    • They should handle field validation cleanly. The REST input uses a '<model>Data.scala' format for dumb data transfer objects, and '<model>.scala' for validated data, each knowing how to produce the other:

    • Ideally they can support both JVM and Javascript platforms for the APIs and models. The cask template is the only cross-platform template I'm aware of which can caters for interesting use-cases, like reusing validation on the client-side, running your servers in the browser for tesing or demos like I've done here and here:


When should I use it? Why should I care?


I've just laid out some of the technical rationale and differences of this new cask generator.


Ultimately though there has to be a business justification for any technology choice. The reasons I think this matter are:

  • Tech is an enabler of different ideas and needs. It's often easier to just keep adding to existing services than to create new ones. Doing that too much can lead to a poor separation of concerns, code bloat, accidental complexity (complexity introduced by the technologies used, rather than present in the core business problem)

  • Being able to try new things quickly and easily fundamentally changes the way you work

  • A good metric for codebase health is how quickly new team members are able to be productive. Keeping codebases small, single-purposed and simple is a big step in that direction. Even if they're unfamiliar with scala, that's why I find Li's approach to his libraries to be the sweet spot for scala's expressiveness and simplicity, while still being feeling familiar to python developers

ideas
It's just all about being able to try more ideas and approaches

How can I try it out?


What follows are the steps for a simple example which only requires docker. We will:

  1. Create a basic service

    1. As for any openapi service, we'll need a service spec and a config

  2. Run the generator on our service to produce our cask server stubs

  3. Package / the boilerplate server code into a jar

  4. Bootstrap our project using the example and that jar.


Step 1: Create an api.yml service and a openapi-config.yml


Any openapi project will need an API spec and some config for the generated code, so let's create an api.yaml for our 'hello' service and a basic 'openapi-config.yml'.


The snippet below will work to for a basic 'hello' service in an 'api.yaml' file:

cat <<EOF > api.yaml
openapi: 3.1.0
info:
  title: Hello API
  description: A simple API example.
  version: 1.0.0
paths:
  /hello:
    get
      summary: Returns a greeting message
      responses:
        '200':
          description: A JSON object containing the message.
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: 'Hello, World!'
EOF

Now create an openapi-config.yml which defines some package namespaces and artifacts:

cat <<EOF > openapi-config.yaml
artifactId: helloworld
artifactVersion: "0.0.1"
groupId: acme
packageName: demo
modelPackage: demo.model
apiPackage: demo.api
gitRepoId: whatever
gitUserId: your-github-user
EOF

Step 2: Generate the server stubs using openapi-generator


With our api.yml and openapi-config.yml from above, we can generate our server project using the new scala-cask generator:

docker run --rm -v "${PWD}:/local" openapitools/openapi-generator-cli:latest generate \
  -i /local/api.yaml \
  -g scala-cask \
  -c /local/openapi-config.yaml \
  -o /local/server

That should produce our server code in the ./server directory.


Step 3: publish the generated code as a jar


The cask template generates both sbt and mill build scripts, so you can build and publish your jar with either.


Rather than publishing to your whatever repo manager you like (artifactory, nexus, github packages, etc), we're just going to publish locally (e.g. just to your own machine):


## publish with sbt
cd server && sbt "project appJVM" publishLocal

## ...or with mill
cd server && mill _.publishLocal

## or use a docker image to build/publish:
docker pull sbtscala/scala-sbt:eclipse-temurin-17.0.4_1.7.1_3.2.0
cd server
docker run --rm \
  -v $PWD:/app \
  -v ~/.ivy2:/root/.ivy2 \
  -v ~/.sbt:/root/.sbt \
  -w /app sbtscala/scala-sbt:eclipse-temurin-17.0.4_1.7.1_3.2.0 \
  sbt "project appJVM" publishLocal

Either way, it should publish your 'hello' server locally:

which we can check:


Step 4: Bootstrap our app


The server generation in step 2 above also produced an 'example' subdirectory which contains two files:

  • Server.scala for our app (business logic) which we an build using scala-cli

  • Dockerfile (to package it up)


Since we've already packaged and published the server stub jar, we can now just move that 'example' directory to somewhere which will be the basis of our project:

cp ./server/example my-project

The original Server.scala had a lot of comments on how to use it/build it, but if we remove that, it's just this:


It's 16 lines of code all-in. It contains:

  • A dependency on the stubs we packaged (line 2)

  • The business logic for the service we defined on lines 8-10. This is what I manually added

  • The main entry point for running it (lines 15-16), which passes the service to the BaseApp which contains the routes.


Let's run it using scala-cli:

cd my-project
# one-line build and run with scala-cli
scala-cli Server

And test the results in the browser:



Summing up


There's plenty more to explore, but hopefully I've show how quick and easy creating new services can be using scala, scala-cli, and the new openapi-generator for cask.


I hope you've found something useful here, and I'd love to hear your thoughts. If you'd like to chat. This is still at alpha-quality, so please do kick the tyres and provide feedback.


Equally if you'd like some help or to chat about how this might work for you, I provide digital coaching and consulting services (but only after we've had a discussion and established if I can add value).


678 views0 comments

Comentarios


bottom of page