FretLink / @clementd
Hoist Me Up Before You Lift Lift
dev haskell, former CTO, I've worked with serveral langages, I used scala a lot (not anymore).
Nowadays, haskell, rust, JS (a bit)
Makes shipment simpler (logistics platform for shipment planning and tracking).
Started with nodeJS / MongoDB, new services are using Haskell / PostgreSQL
Servant, handlers, natural transformations
Servant is an collection of libraries designed to work with HTTP APIs
The core is a type-level DSL to model APIs. You can then provide a server or generate a client.
You can debug an api layout (not exactly this output, but close)
Today I’ll talk about servers. servant-server allows you to get a WAI application based on the API description (and handlers)
Handler allows you to do IO, and to return non-200 HTTP codes with
throwError. It focuses on the data types, rather than HTTP itself
Parameters extracted from the routes are passed as function arguments
Server represents a collection of handlers, matching an API type. It’s not a “real” type, but a type family.
Server inspection with
λ> :kind! Server API
= Handler [User]
:<|> ((MkUser -> Handler NoContent)
:<|> (Int -> Handler User
:<|> Handler NoContent))
Server API is not a real type, in case of doubt, use
:kind! to know what you’re actually dealing with. kind “evaluates” type families instances
With a server, you can generate a WAI application (and serve it with warp for instance). all the WAI middlewares are compatible.
Proxy is is a way to feed a type to a function without an accompanying value. It’s used a lot by servant, and it avoids using undefined when all we’re interested in is the type
TypeApplications extension is quite useful in this context, I’ll use it from now on to have terser code
That’s all you need to use servant
With that, you’re already able to create services and structure APIs.
Dependency injection with Reader
It’s common for our handlers to need a few dependencies: common config, access to a DB pool, things like that. A standard way to do that is to use the Reader Monad
In our case, we’ll need the base URL (to construct absolute URLs) and access to the DB pool
This way we can access the DB pool in our handlers. Since
Handler is already a monad, we use the transformer version of Reader, to add Reader capabilities to the handler.
Server is specialized for
Handler, so we need to use the more general
Wiring it all up
And now the magic. We wrap everything in
hoistServer, and we provide a function transforming
Handler. In our case it’s
HasServer is servant’s internal type-families-based machinery. What’s important is that we can go from a handler m to handler n. In our case, m is
MyHandler, n is
Handler. We can put all our endpoints in the monads we want as long as we end up with a
forall x. m x -> n x
Note that it does not mention
Handler at all. So we can chain as many transformations as we want, as long as the last one gives us a
Chaining handler transformations
We’ll keep our dependency injection, but we’ll add user capabilities with servant-auth, and we’ll add admin-only endpoints
HasAdmin wraps around our custom handler. It allows us to declare endpoints with extended capabilities. I’ve omitted all the instances derivation, to let it delegate to the inner handler.
Given a user, and an admin-only handler, we can either delegate to the handler or generate an error. Note the return type (I’ve added parens for clarity)
All the endpoints are now protected with basic auth (the endpoints will take a User parameter) The protected endpoints are either regular (all users) or admin-only
The main server is the same, it handles the reader monad. It also passes the user down to the other servers (it could also be put in the reader, but I chose not to, for clarity).
Pay special attention to the type annotations (especially the API vs UserEndpoints). It’s not intuitive, and it’s easy to be trapped (I sure was). Discuss it with the audience
The user endpoints don’t change, but for the admin endpoint, we need to peel out the handler from the HasAdmin wrapper. We can do so by using the previously defined ensureAdmin.
/ -- MyHandler
┆ ├─• GET
┆ ├─• POST
┆ └─ <userId>/
┆ ├─• GET
┆ └─• DELETE
└─ admin/ -- HasAdmin
Application -> Application
Not suited for application-level stuff (not visible in the types). Kludges like vault. It’s good protocol- level stuff (HTTP redirs, etc), but not much more, and it’s for the whole application (or you need to inspect the requests in the middlewares and that’s a shadow router. not good)
- don’t thread environment manually,
Most common use case, it comes up fast in every non-trivial service
- define a monad stack for your application
Try to standardize on a monad stack, avoid ad-hoc stuff, it’ll simplify things and make maintenance easier
- annotate / protect whole API trees with
For application-level concerns, it’s way better than regular middlewares, and it retains type-safety
- mind the Proxy type annotations
That’s the most common pitfall, and it’s easy to get lost in servant’s type errors
Do you have questions?