Noel Welsh London Scala User Group 18 June 2025
GET /user/:id controllers.Users.show(id: Int)
External DSL
HttpRoutes.of[IO] { case GET -> Root / "user" / IntVar(userId) => Ok(s"You asked for $userId") }
Pattern matching
endpoint .in("user" / path[Int]) .out(stringBody)
Combinator library
val handler = Route( Request.get(Path / "user" / Param.int), Response.ok(Entity.text) ).handle(userId => s"You asked for $userId")
Route( Request.get(Path / "user" / Param.int), Response.ok(Entity.text) ).handle(userId => s"You asked for $userId")
Request.get(Path / "user" / Param.int),
Output elements from the HTTP request. (Here, an Int.)
Int
Response.ok(Entity.text)
Generate a HTTP response from input. (Here, a String.)
String
userId => s"You asked for $userId"
Request, Response, and Path are values that we can compose.
Request
Response
Path
val api = Path / "api" / "v1" val users = api / "users" val products = api / "products" val notFound = Response.staticFile("/etc/assets/not-found.html") Response.ok(Entity.html).orElse(notFound)
val viewUser = Route( Request.get(Path / "user" / Param.int), Response.ok(Entity.text) ) viewUser.pathTo(1234) // "/user/1234"
Create links to embed, for example, in HTML
val viewUser = Route( Request.get(Path / "user" / Param.int), Response.ok(Entity.text) ) viewUser.client(1234) // IO[String]
Call routes from another server, or from the browser. Work-in-progress
Path / "user" / Param.int // Extracts an Int Path / "order" / Param.uuid / "item" / Param.int // Extracts a UUID and an Int
Path[A] Path[A, B] Path[A, B, C] // etc...
This approach doesn't work.
Path[(A)] Path[(A, B)] Path[(A, B, C)]
This approach does work. But how?
Path[P <: Tuple]
There is a common super type for all tuples.
class Path[P <: Tuple] { /** Add a segment that extracts a parameter to this `Path`. */ def /[B](param: Param[B]): Path[Tuple.Append[P, B]] = ??? }
Tuple.Append[P, B] is the type of the tuple type P with the type B appended to it. E.g. if P is (Int, String), Tuple.Append[P, Double] is (Int, String, Double).
Tuple.Append[P, B]
P
B
(Int, String)
Tuple.Append[P, Double]
(Int, String, Double)
Easy to implement with match types.
Already defined in the standard library.
TupleApply
Tuple
trait TupleApply[I, O] { type Fun def tuple(f: Fun): I => O } object TupleApply { given tuple1Apply[A, C]: TupleApply[Tuple1[A], C] with { type Fun = A => C def tuple(f: A => C): Tuple1[A] => C = a => f(a(0)) } // etc ... }
Replace with TupledFunction if/when it's no longer experimental.
TupledFunction
/** A [[krop.route.Request]] describes a pattern within a [[org.http4s.Request]] ... * * @tparam P * The type of values extracted from the URI path. * @tparam Q * The type of values extracted from the URI query parameters. * @tparam I * The type of values extracted from all parts of the request, including path * and query. * @tparam O * The type of values to create a [[org.http4s.Request]] that matches this * [[krop.route.Request]]. */ sealed abstract class Request[P <: Tuple, Q <: Tuple, I <: Tuple, O <: Tuple]
We need to know different groups of parameters for reverse routing:
val jsonContentType = `Content-Type`(MediaType.application.json) Request.post(Path / "user") .ensureHeader(jsonContentType) .withEntity(Entity.jsonOf[User])
Must specify components of a Request in a specific order.
Fixed order:
Request is a (not quite) algebraic data type:
RequestMethodPath
RequestHeaders
RequestENtity
Methods transition between these types, forming a simple finite-state machine.
final case class RequestMethodPath[P <: Tuple, Q <: Tuple] { /** When matching a request, the given header must exist in the request and * it will be extracted and made available to the handler. * * When producing a request, a value of type `A` must be provided. */ def extractHeader[A](using h: Header[A, ?], s: Header.Select[A] ): RequestHeaders[P, Q, Tuple1[s.F[A]], Tuple1[s.F[A]]] = RequestHeaders.empty(this).andExtractHeader[A] }
What is best?
Technical consideration. Is routing built from reusable pieces?
Technical consideration. Can routes be converted into links and clients?
Code is (mostly) written by humans. What is it like to use the library? Can a junior developer make sense of it? This is more subjective.
Can I press . and follow the completions?
.
External DSL. IDE doesn't know about it.
Pattern matches don't get completions.
Methods like path and stringBody are global imports.
path
stringBody
Methods and values are found on companion objects.
Krop is a bit more verbose than Tapir. Follow the types. Need a Request? Type Request press . and follow completions.
Krop is a bit more structured than Tapir. Allows more flexible reverse routing. Makes routes easier to read.
What happens when it goes wrong? Many decisions affect debugging.
In development mode, when no route matches a helpful page is shown.
Param.int instead of param[Int]. Simpler error messages with equivalent usability.
Param.int
param[Int]
Use @implicitNotFound annotation to give useful error message when given values are used.
@implicitNotFound
given
Use Scala 3 features, instead of equivalent but more obscure features in, e.g., Shapeless
Bring joy into the world!