http4k - a Kotlin FaaS library
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
HttpHandler
s can be bound to a container with theasServer
method. This creates anHttp4kServer
, as follows:
val jettyServer = app.asServer(Jetty(9000)).start()
RoutingHttpHandler
, which is both anHttpHandler
and aRouter
. It’s the result of “binding” anHttpHandler
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") }
RoutingHttpHandler
s 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") }
)
Router
s 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")))
}