Loading
FretLink / @clementd

Hoist Me Up Before You Lift Lift

$ whoami

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

Servant is an collection of libraries designed to work with HTTP APIs
type API = "users" :>
     ( Get '[JSON] [User]
  :<|> ReqBody '[JSON] MkUser
         :> Post '[JSON] NoContent
  :<|> Capture "userId" UserId :> 
       ( Get '[JSON] User
    :<|> Delete '[JSON] NoContent
       )
     )
The core is a type-level DSL to model APIs. You can then provide a server or generate a client.
/
└─ users/
   ├─• GET
   ├─• POST
   └─ <userId>/
      ├─• GET
      └─• DELETE
You can debug an api layout (not exactly this output, but close)

Servant Server

Today I’ll talk about servers. servant-server allows you to get a WAI application based on the API description (and handlers)
listUsers :: Handler [User]
listUsers = liftIO getAllUsers

createUser :: MkUser -> Handler NoContent
createUser =
    liftIO addUser >=>
      either handleError handleSuccess
  where
    handleError _ = throwError err400
    handleSuccess _ = pure NoContent
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
getUser :: UserId -> Handler User
getUser =
  liftIO getUser >=>
    maybe (throwError err404) pure
Parameters extracted from the routes are passed as function arguments
handlers :: Server API
handlers =
    allUsers :<|> singleUser
  where
    allUsers =
      listUsers :<|> createUser
    singleUser userId =
      getUser userId :<|> deleteUser userId
Server represents a collection of handlers, matching an API type. It’s not a “real” type, but a type family.

Server inspection with :kind!

λ> :kind! Server API
= Handler [User]
  :<|> ((MkUser -> Handler NoContent)
          :<|> (Int -> Handler User
                  :<|> Handler NoContent))
Never forget, 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
app :: Application
app = serve api handlers
  where
    api :: Proxy API
    api = Proxy
With a server, you can generate a WAI application (and serve it with warp for instance). all the WAI middlewares are compatible.

Proxy





data Proxy a = Proxy
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
{-# LANGUAGE TypeApplications #-}
app :: Application
app = serve @API Proxy handlers
The TypeApplications extension is quite useful in this context, I’ll use it from now on to have terser code

app :: Application
app = serve api handlers
  where
    api :: Proxy API
    api = Proxy

app :: Application
app = serve (Proxy :: Proxy API) handlers
{-# LANGUAGE TypeApplications #-}
app :: Application
app = serve @API Proxy handlers

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
-- We need a few things
data Env = Env
  { baseUrl :: Url
  , pool :: DbPool
  }
In our case, we’ll need the base URL (to construct absolute URLs) and access to the DB pool
type MyHandler = ReaderT Env Handler

getAllUsers :: DbPool -> IO [User]

listUsers :: MyHandler [User]
listUsers =
  asks pool
    >>= liftIO . getAllUsers
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.
type MyServer api = ServerT api MyHandler

handlers :: MyServer API
handlers = ... -- same as before
Note that Server is specialized for Handler, so we need to use the more general ServerT version.

Wiring it all up




server :: Env -> Server API
server env =
    hoistServer @API Proxy withEnv handlers
  where
    withEnv :: (MyHandler a -> Handler a)
    withEnv v = runReaderT v env
And now the magic. We wrap everything in hoistServer, and we provide a function transforming MyHandler into Handler. In our case it’s runReaderT
hoistServer :: HasServer api '[]
            => Proxy api
            -> (forall x. m x -> n x)
            -> ServerT api m
            -> ServerT api n
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 Handler.

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 Handler.

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
newtype HasAdmin a =
  HasAdmin (MyHandler a)
  deriving (Monad, MonadReader, …)

deleteEverything :: HasAdmin NoContent
deleteEverything =
  liftIO dropDatabase
    >> pure NoContent
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.
ensureAdmin :: User
            -> (HasAdmin a -> MyHandler a)
ensureAdmin user (HasAdmin handler)
  | isAdmin user = handler
  | otherwise = throwError err403
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)
type API =
  Auth '[BasicAuth] User :> UserEndpoints

type UserEndpoints = Regular :<|> Admin
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
server :: Env -> Server API
server env user =
  hoistServer @UserEndpoints
    Proxy
    withEnv
    (userEps user :<|> adminEps user)

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
adminEps :: User -> MyServer Admin
adminEps user =
  hoistServer @Admin
    Proxy
    (ensureAdmin user)
    deleteEverything
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
├─ users/
┆  ├─• GET
┆  ├─• POST
┆  └─ <userId>/
┆     ├─• GET
┆     └─• DELETE
└─ admin/  -- HasAdmin
   └─ yolo/
      └─• DELETE

Why not middlewares?

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)

Conclusion

Most common use case, it comes up fast in every non-trivial service

Conclusion

Try to standardize on a monad stack, avoid ad-hoc stuff, it’ll simplify things and make maintenance easier

Conclusion

For application-level concerns, it’s way better than regular middlewares, and it retains type-safety

Conclusion

That’s the most common pitfall, and it’s easy to get lost in servant’s type errors

Conclusion

Macaroons with servant

Thanks!

Do you have questions?

(we're hiring)