I'm now working with http4k for a couple of weeks....
# http4k
c
I'm now working with http4k for a couple of weeks. The app is shaping up nicely. And http4k has been an absolute joy to work with! A big thank you to all involved inr open sourcing this. I use it together with terpal-sql and kotlinx.html, to build an SSR app on top of Supabase (which is clearly not Supabase's design goal, but still works nicely). I want to share a few details and questions that i have (casual sharing, no urgent needs/dramas).
๐Ÿ”ฅ 2
I've settled on the following package structure (mainly because then I can Konsist for architecture tests: which package can/cannot import from which other package). This will keep the team from messing up the architecture...
ClaudeCode is good with http4k, as long as you have pre-existing code, and you just ask it basic questions. I was afraid it would do better with more popular frameworks/libs (like, yuck, SpringBoot), but I think it does really well on my rather unconventional stack.
Testing with http4k is a lot of fun (as to be expected from an FP approach).
I use but do not really understand the contextKey system. It seems less functional than simply passing values as arguments (but I can see how that makes it much harder to re-use code). My filters set context and I pick it up in my handlers. Sometimes I make a mistake, and then I get a runtime error. They are easy to catch errors, but still: I prefer to use type safety to eradicate the runtime errorability of my code.
So far I managed to keep the stack minimal. Just 11MB of dependencies! (adding either of SpringBoot/jOOQ/Hibernate would easily double that). Less code = less to learn = faster to release.
I wrote my own www-form-urlencoded form body decoder, that reuses kotlinx.serilization for type coercion (form bodies flat Map<String, String>, my form DTO are structured and the fields have values of many different types). This lib works well; I wonder if I should make it proper member of kotlinx.serilization (instead of merely using it for type coercion), which would allow me to serialize my form DTOs to www-form-urlencoded form bodies, which'd be grand for tests.
I've avoided kotlin-reflection as a (transient) dependency: this speeds up app boot time a lot. Super fast (<1s) development loops if I did not change too much (99% of the time).
I put all my paths in a
Paths
object (added some functions to generate paths with parameters with a clean API). I use these paths in my routes definitions and in my navigation menus + HTML/email templates. This way I can CTRL-click to know where a path is used and change it in one place. This has worked really nice for me.
a
I think Claude is fairly decent at http4k because FP's line-by-line compositional approach is easier for an LLM to reason with. kotlinx.serialization is a good choice for making a small jar; I believe Undertow is the smallest server, and the builtin JavaHttpClient is perfectly capable for production. Making small Jars is a fun challenge in and of itself, but your approach is great for serverless architectures like AWS Lambda. Or if you're using docker, running on alpine with a jlink'd JVM (containing only the modules you need) can get you a an image in the low 70MBs, which is great for fast scaling.
kotlinx.serialization is a great choice, but moshi with the kotshi plugin integrates better into http4k's automarshalling system; especially if you're using values4k classes, which require custom adapters. Kotshi is used extensively in http4k connect.
c
I have a question: what's the overhead of a lens? In outer words, should i bind a lens to local val (e.g.:
val db = dbRequestKey(req)
and use that for several calls instead of looking through the lens every call) or does it not matter since the overhead is little or even less then the overhead of the local val?
d
We haven't measured it, but I suspect the effect of the lens is not worth worrying about and probably gets optimised by the JVM, Previously, I used to extract the lenses if it made sense and they were reusable. Now since we have
request.json<T>()
and others like that (where json is an inlined function with a lens construction inside), I don't really bother TBH.
c
Thanks @dave... I was considering to do the same with the extension function on request ๐Ÿ™‚ just looks more kotlinesque with the method-call syntax over the funciton-call syntax.
a
FWIW, the
json
function on
ConfigurableMoshi
doesn't properly reify the type paramter. It eventually resolves it into a
T::class
in
autoBody
, which breaks generics.
d
Generics are always hairy when it comes to serialisation ๐Ÿคท๐Ÿผโ€โ™‚๏ธ ๐Ÿ™ƒ
a
Darn page objects
c
So I've worked with several other frameworks ๐Ÿ™‚ (good start of a story innit?) and they usually had (1) lost of globals, (2) no type-safety (which simplifies things a lot, at first), do crazy annotation magic. or (3) push for some sort of DI (welcome the God object). http4k does different. i like it. but I put a lot of lenses on the request... I've seen the "Filter" pattern before (Rack in Ruby-land and in many Rust web frameworks), and I like it. But now I'm using it to pass config constants through the code... So I merely want to check if that's a common pattern... That's all. ๐Ÿ’š
d
Not sure what you mean here - it does sound not-common on the face of it but I may be misunderstanding. Can you give us an example? If you want to see "the norm" - or at least as we do it - suggest taking a look at the http4k-by-example repo. Note that a lot of how we build apps with http4k is very very different to what is "standard" in the general JVM community.... ๐Ÿ˜‰ Also worth checking out our KotlinConf talks (you can see the "in action" page on the site for these) to see what is possible.
We do have a typesafe config system (http4k-config) which is based around lenses. I especially recommend checking out the concepts in the Exploring the Testing Hyperpyramid talk for an example of separating the hexagonal ports from the sources of randomness
c
Thanks, will have a look!
a
You shouldn't have to pass config objects around. If you're architecting very functionally, then you might not be using enough closures to inject the config into your functions. Layered architectures make it pretty natural to keep cohesion. I usually do it like this:
Copy code
fun createApp(
  env: Environment,
  internet: HttpHandler
) = App(
  storage = JdbcStorage(env[Config.jdbcUri]),
  pageSize = env[Config.pageSize])
)

fun App.toApi() = contract {
  routes += ...
}

fun main(
  createApp(Environment.ENV, JavaHttpClient())
    .toHttp()
    .asServer(Undertow(80))
    .start()
)

fun testApp() = createApp(
  env = Environment.withDefaults(
    Config.jdbcUri of h2DatabaseUri(),
    Config.pageSize of 10
  ),
  internet = reverseProxy(
    "service1" to FakeService(),
    "service2" to ClientFilters.SetHost(dockerContainer.uri).then(JavaHttpClient())
  )
)
I have a relatively short youtube

seriesโ–พ

on building an api with http4k and an example repo, with the main function being a good place to start.
c
Ok, I'll have a look... But quick question: how to get the pageSize value in a handler?
a
In an
HttpHandler
? There's no need in a layered architecture, because it's only a concern of your business logic (i.e. the App), and the HttpHandler has the App because you can build a function called
fun App.toApi
, which builds to HttpHandler.
Copy code
class App(
  private val pageSize: Int,
  private val storage: Storage
) {
  fun listWidgets(accountId: String, cursor: String?) = storage.listWidgets(
    accountId = accountId,
    pageSize = pageSize,
    cursor = cursor
  )
}

// Note: this is using the httpk4-api-openapi syntax to define a handler
fun App.toApi() = contract {
  routes += "/v1/accounts" / accountIdLens / "widgets" {
     queries += cursorLens
  } bindContract Get to { accountId, _ -> {
    { request ->
      val result = listWidgets(accountId, cursorLens(request))
      Response(OK).with(widgetsLens of result)
    }
  }
}
and in a functional architecture, you could do something like
Copy code
fun listWidgetsFactory(
   env: Environment
): HttpHandler {
  val storage = JdbcStorage(env[Config.dataSource])
  return { request ->
    val result = storage.listWidgets(
      accountId = accountIdLens(request),
      pageSize = env[Config.pageSize],
      cursor = cursorLens(request)
    )
    Response(OK).with(widgetsLens of result)
  }
}

fun createApp(env: Environment): HttpHandler {
  return routes(
    "accounts/$accountIdLens/widgets" bind GET to listWisgetsFactory(env)
  )
}
So how do I get the pageSize in? Well, there's a lens for that in http4k-config, and you just make sure you set the ENV when you run the program. It also supports parsing YAML files. Or just use
System.getEnv
to get properties directly from the system env.
Copy code
object Config {
  val pageSizeLens = EnvironmentKey.int().required("PAGE_SIZE")
}
As some general advice, frameworks trick you into thinking there's a specific way you need to do things to make an app. Http4k is just a library, so you have to architect your own app around it. Unfortunately, this trips up a lot of people ๐Ÿ˜… .
๐Ÿ’ฏ 1
c
Ok ok. So it's okay to have config as a global (i had that but then thought it was better not to, because of it implicitly sharing data throughout my whole app code).
Thanks!
a
No, it's just the lenses that are global (which are stateless) The config is extracted from the ENV, property by property with
http4k-config
. Keeping config as a global may work most of the time, but if you ever run into a scenario where it's a problem, then it's real tough to unwind.
It might be easier to think of
Config.pageSize
as
pageSizeConfigLens
, but instead of extracting things from a request or response, it's from an
Environment
from
http4k-config
.
When I call
Environment.ENV
, it's actually just a helper function for
Environment(System.getEnv())
Not long ago, I was actually talking to a prospective new employer about why config shouldn't be global, and had a hard time articulating it. Like I said, most of the time, it's not really a problem. But if you get into things like websocket clusters, then it will completely fall apart during testing. It's also much safer to have non-global config for tests regardless; otherwise they can pollute each other. So imo, it's good hygiene to just not rely on global config at all.
๐Ÿ’ฏ 2
c
That's what i do right now. I put all values from config that are needed throughout the code (most config values are only used at startup) on the request context.
a
I'm not certain why it needs to be put on the request context.
c
Me neither, lol...