Upgrade to Pro — share decks privately, control downloads, hide ads and more …

http4k: Server as a Function. In Kotlin.

http4k: Server as a Function. In Kotlin.

In this talk, we show how to combine functional programming concepts and the versatility of Kotlin to produce applications that are simpler and more testable. And the best of it: with absolutely no magic!

Ivan Sanchez

October 18, 2022
Tweet

More Decks by Ivan Sanchez

Other Decks in Programming

Transcript

  1. Server as a Function • 2013 white paper from Marius

    Eriksen @ Twitter • Composing of Services using just 2 types of asynchronous function: • Service - Represents a system boundary (symmetric) • Filter - aka middleware - Models application agnostic concerns
  2. http4k Principles • Easily testable. • No magic: No re

    fl ection. No annotations. • Minimize dependencies. • Starts/stops quickly. • Immutable HTTP model. • Leverage Kotlin.
  3. Concept: HttpHandler “It turns an HTTP Request into an HTTP

    Response” HttpHandler: (Request) -> Response ie. it’s a function! val echo: HttpHandler = { req: Request -> Response(OK).body(req.body) } val resp: Response = echo(Request(POST, “/echo“).body(“hello”))
  4. Concept: Filter “Provides pre and post processing on an HTTP

    operation” Filter: (HttpHandler) -> HttpHandler ie. it’s a function! val twitterFilter = Filter { next: HttpHandler -> { req: Request -> val tweet: Request = req.body(req.bodyString().take(140)) next(tweet) } } val tweet: HttpHandler = twitterFilter.then(echo)
  5. Concept: Router Router: (Request) -> HttpHandler? ie. it’s a function!

    “Matches an HttpHandler against a Request” val routes: HttpHandler = routes( "/echo" bind POST to echo, "/twitter" bind routes( “/tweet” bind POST to tweet ) ) Can compose multiple routers to make an HttpHandler http4k does a depth- fi rst search on the tree, then falls back to 404
  6. Serving HTTP val echo: HttpHandler = { r: Request ->

    Response(OK).body(r.body) } val server: Http4kServer = echo.asServer(Undertow(8000)).start() We can attach an HttpHandler to a running container
  7. Consuming HTTP HttpHandler: (Request) -> Response We can reuse the

    symmetric HttpHandler API: val client: HttpHandler = ApacheClient() val response: Response = client( Request(GET, "https://www.http4k.org/search") .query("term", “http4k is cool") )
  8. Extreme Testability Testing http4k apps is trivial because: • the

    building blocks are just functions • messages are immutable data classes! val echo: HttpHandler = { r: Request -> Response(OK).body(r.body) } class EchoTest { @Test fun `handler echoes input`() { val input: Request = Request(POST, "/echo").body("hello") val expected: Response = Response(OK).body("hello") assertThat(echo(input), equalTo(expected)) } }
  9. Extreme Testability Testing http4k apps is trivial because: • the

    building blocks are just functions • messages are immutable data classes! val echo: HttpHandler = SetHostFrom(Uri.of("http://myserver:80")) .then(ApacheClient()) class EchoTest { @Test fun `handler echoes input`() { val input: Request = Request(POST, "/echo").body("hello") val expected: Response = Response(OK).body("hello") assertThat(echo(input), equalTo(expected)) } }
  10. Typesafe HTTP? • How do we enforce our incoming HTTP

    contract? • Locations: Path/Query/Header/Body/Form • Optionality - required or optional? • Marshalling + Typesafety • What about creating outbound messages? val miner: HttpHandler = routes( "/mine/{btc}" bind POST to { r: Request -> val newTotal: Int = r.path("btc")!!.toInt() + 1 Response(OK).body("""{"value":$newTotal}""") } )
  11. Concept: Lens “A Lens targets a speci fi c part

    of a complex object to either GET or SET a value” ie. it’s a function - or more precisely 2 functions! Extract: (HttpMessage) -> X Inject: (X, HttpMessage) -> HttpMessage • these functions exist on the lens object as overloaded invoke() functions
  12. Lens example • Revisiting the earlier example… val miner: HttpHandler

    = routes( "/mine/{btc}" bind POST to { r: Request -> val newTotal: Int = r.path("btc")!!.toInt() + 1 Response(OK).body("""{"value":$newTotal}""") } ) data class BTC(val value: Int) { operator fun plus(that: BTC) = BTC(value + that.value) override fun toString() = value.toString() } • Let’s introduce a domain type to wrap our primitive val btcPath: PathLens<BTC> = Path.int().map(::BTC).of("btc") val btcBody: BiDiBodyLens<BTC> = Body.auto<BTC>().toLens() • … and create lenses to do automatic marshalling:
  13. val btcPath: PathLens<BTC> = Path.int().map(::BTC).of("btc") val btcBody: BiDiBodyLens<BTC> = Body.auto<BTC>().toLens()

    val miner: HttpHandler = CatchLensFailure.then( routes( "/mine/{btc}" bind POST to { r: Request -> val newTotal: BTC = btcPath(r) + BTC(1) btcBody(newTotal, Response(OK)) } ) ) Lens example: before & after val miner: HttpHandler = routes( "/mine/{btc}" bind POST to { r: Request -> val newTotal: Int = r.path("btc")!!.toInt() + 1 Response(OK).body("""{"value":$newTotal}""") } )
  14. Business Abstraction Remote Client Launcher Load Environment Con fi guration

    Embedded Server Launch Application Stack Logging Metrics Remote Clients Application Business Abstraction Business Abstraction The Layer Cake Route Route Route Route
  15. Standardised Server & Clients fun serverStack(systemName: String, app: HttpHandler): HttpHandler

    = logTransactionFilter(“IN”, systemName) .then(recordMetricsFilter(systemName)) .then(handleErrorsFilter()) .then(app) fun clientStack(systemName: String): HttpHandler = logTransactionFilter(“OUT”, systemName) .then(recordMetricsFilter(systemName)) .then(handleErrorsFilter()) .then(ApacheClient()) Filter.then(that: Filter) -> Filter By utilising the ability to “stack” Filters, we can build reusable units of behaviour
  16. Fake Your Dependencies! Fake HTTP Service State • Leverage Body

    lenses • Simple state-based behaviour • Run in memory or as server • Easy to simulate failures
  17. Test Environment Con fi guration Application Testing • We can

    simply inject apps into each other in order to build an o ffl ine environment • Using fakes, we can inject failure to test particular scenarios • All internal and external applications and clients are HttpHandlers
  18. Fakes by Contract Abstract Test Contract Success scenarios Business Abstraction

    Fake Dependency Test Fake System (HttpHandler) State Failure scenarios Real Dependency Test Environment Con fi guration Remote Client (HttpHandler)
  19. CDD: Chaos-Driven-Development • By decorating HttpHandler with a Filter, we

    can inject bad runtime behaviour to simulate failures • Add pre-canned bad-behaviour using the ChaosEngine val chaosStage: Stage = Latency().appliedWhen(Always()) val app: HttpHandler = { req: Request -> Response(OK) } app.withChaosEngine(chaosStage).asServer(Undertow(8000)).start() … or add it to a running service and con fi gure it remotely using the ChaosEngine OpenAPI interface!
  20. serverless4k Http4k Apps can run as Serverless functions by implementing

    a single interface ** http://bit.ly/coldstartwar AWS API Gateway AWS Lambda
  21. native4k •GraalVM is a universal VM •Can compile JVM apps

    into native binaries •Small size + quick startup •BUT: No simple re fl ection = hard for many libs •http4k apps with Apache-backend work out of the box! •Simple Graal http4k app = 6mb •With Docker (Alpine), 9mb
  22. About the project • v1 released in May 2017 •

    570 releases, 6k commits, ~100 contributors • 66 modules, supporting 17 server/serverless/native backends • Core module is still only 1mb (with zero dependencies) • One of the top Kotlin frameworks in TechEmpower benchmarks • Featured in JetBrains “Serverside Kotlin” Webinar series* *https://www.youtube.com/watch?v=NjoCjupV8HE
  23. http4k nanoservices “5 useful mini-apps which all fi t in

    a tweet!” { ws: Websocket -> while (true) { ws.send(WsMessage( Instant.now().toString()) ) Thread.sleep(1000) } }.asServer(Jetty()).start() ProxyHost(Https) .then(JavaHttpClient()) .withChaosEngine(Latency() .appliedWhen(Always)) .asServer(SunHttp()) .start() static(Directory()) .asServer(SunHttp()) .start() ProxyHost(Https) .then(RecordTo(Disk("store"))) .then(JavaHttpClient()) .asServer(SunHttp()) .start() JavaHttpClient().let { client -> Disk("store").requests() .forEach { println(it) client(it) } } http://bit.ly/http4k-nanoservices