http4k - a Kotlin FaaS library

May 30, 2019 4 minutes
http4k logo

These are some notes I took while trying the http4k library. I concluded that it was the epitome of good taste in library design, and definitively something I would like to use again.

Main concepts

  • HttpHandler: (Request) -> Response, somewhat equivalent to an Action in ASP.NET MVC.
  • Filter: (HttpHandler) -> HttpHandler, Request/Response pre/post processing. Many filters can be stacked together, of course.
  • Router: (Request) -> HttpHandler?, determines which handler can handle a given request, if any. It can do this match based on any attribute of the request itself, instead of just URI path or HTTP method.

Additional concepts

  • HttpHandlers can be bound to a container with the asServer method. This creates an Http4kServer, as follows:
    val jettyServer = app.asServer(Jetty(9000)).start()
  • RoutingHttpHandler, which is both an HttpHandler and a Router. It’s the result of “binding” an HttpHandler to a path and HTTP verb.
  • Lens which turns requests into some kotlin object, and serializes back that object into something else (e.g. JSON, XML).

Cookbook

Refresher of some concepts related to using http4k.

Package names

Current version of all of those: 3.146.0 3.244.0.

https://search.maven.org/search?q=org.http4k

  • org.http4k:http4k-core
  • org.http4k:http4k-server-undertow
  • org.http4k:http4k-template-freemarker

Mount a Handler in some server

myApp.asServer(Undertow(8000)).start()

Here the example uses the Undertow server. An alternative for development that comes with the core install is SunHttp.

Routing

val route: RoutingHttpHandler = "/path" bind GET to { Response(OK).body("you GET bob") }

RoutingHttpHandlers can be grouped together:

val app: RoutingHttpHandler = routes(
    "bob" bind GET to { Response(OK).body("you GET bob") },
    "rita" bind POST to { Response(OK).body("you POST rita") },
    "sue" bind DELETE to { Response(OK).body("you DELETE sue") }
)

Routers can be combined together to form another RoutingHttpHandler; routes can be nested:

val app: HttpHandler = routes(
    "/app" bind GET to decoratedApp,
    "/other" bind routes(
        "/delete" bind DELETE to { _: Request -> Response(OK) },
        "/post/{name}" bind POST to { request: Request -> Response(OK).body("you POSTed to ${request.path("name")}") }
    )
)

Lens - typesafe HTTP

Automatic mode

Examples, and data classes generation tool

package json

import org.http4k.core.Body
import org.http4k.core.Method.GET
import org.http4k.core.Request
import org.http4k.format.Jackson.auto

// this JSON...
val json = """{"jsonRoot":{"child":["hello","there"],"num":123}}"""

// results in these data classes...
data class Base(val jsonRoot: JsonRoot?)
data class JsonRoot(val child: List<String>?,
                    val num: Number?)

// use the lens like this
fun main() {
    val lens = Body.auto<Base>().toLens()
    val request = Request(GET, "/somePath").body(json)
    val extracted: Base = lens.extract(request)
    println(extracted)
    val injected = lens.inject(extracted, Request(GET, "/somePath"))
    println(injected.bodyString())
}

Also, according to https://www.http4k.org/guide/modules/json/ , it’s possible to omit the extract and inject method calls when using lenses, as follows:

// inject
val requestWithEmail = messageLens(myMessage, Request(GET, "/"))

// extract
val extractedMessage = messageLens(requestWithEmail)

Both of these examples also work with Response objects, as per the documentation.

Manual mode

Note the exception handling here. Exceptions are raised when there’s an error retrieving a given thing from the request (e.g. type mismatch, or missing argument).


fun main() {

    data class Child(val name: String)

    val nameHeader = Header.required("name")
    val ageQuery = Query.int().optional("age")
    val childrenBody = Body.string(TEXT_PLAIN).map({ it.split(",").map(::Child) }, { it.map { it.name }.joinToString() }).toLens()

    val endpoint = { request: Request ->

        val name: String = nameHeader(request)
        val age: Int? = ageQuery(request)
        val children: List<Child> = childrenBody(request)

        val msg = "$name is ${age ?: "unknown"} years old and has " +
            "${children.size} children (${children.map { it.name }.joinToString()})"
        Response(Status.OK).with(
            Body.string(TEXT_PLAIN).toLens() of msg
        )
    }

    val app = ServerFilters.CatchLensFailure.then(endpoint)

    val goodRequest = Request(Method.GET, "http://localhost:9000").with(
        nameHeader of "Jane Doe",
        ageQuery of 25,
        childrenBody of listOf(Child("Rita"), Child("Sue")))

    println(listOf("", "Request:", goodRequest, app(goodRequest)).joinToString("\n"))

    val badRequest = Request(Method.GET, "http://localhost:9000")
        .with(nameHeader of "Jane Doe")
        .query("age", "some illegal age!")

    println(listOf("", "Request:", badRequest, app(badRequest)).joinToString("\n"))
}

Server-side templates

https://www.http4k.org/guide/modules/templating/

Freemarker: org.http4k.http4k-template-freemarker

data class Person(val name: String, val age: Int) : ViewModel

fun main() {

    // first, create a Renderer - this can be a Caching instance or a HotReload for development
    val renderer = HandlebarsTemplates().HotReload("src/test/resources")

    // first example uses a renderer to create a string
    val app: HttpHandler = {
        val viewModel = Person("Bob", 45)
        val renderedView = renderer(viewModel)
        Response(OK).body(renderedView)
    }
    println(app(Request(Method.GET, "/someUrl")))

    // the lens example uses the Body.view to also set the content type, and avoid using Strings
    val viewLens = Body.viewModel(renderer, ContentType.TEXT_HTML).toLens()

    val appUsingLens: HttpHandler = {
        Response(OK).with(viewLens of Person("Bob", 45))
    }

    println(appUsingLens(Request(Method.GET, "/someUrl")))
}