HTTP APIs with WebGear

First of all, you should check out the user guide if you are not familiar with WebGear. This is a short blog post and I won’t be covering everything in detail.

Haskell for HTTP APIs

Most APIs accept some input from the HTTP request, perform a bunch of validations and operations, and then respond with some data retrieved from a backend such as a database or other services. Haskell has some unique strengths that makes this whole process robust.

I strongly believe in making illegal states unrepresentable in the types and techniques like “parse, don’t validate”. Haskell is well-suited for this style of programming and WebGear implements some of these ideas in building HTTP APIs.

Type Safety

Haskell’s type system makes it possible to encode concepts at type level and WebGear makes good use of this. For example, here is a handler:

-- Use this handler for a route such as: PUT /api/widget/<widgetId>
putWidgetHandler :: Have [ Method PUT
                         , PathVar "widgetId" Int
                         , JSONRequestBody Widget
                         , BasicAuth
                         ] req
                 => Handler req Widget
putWidgetHandler = ...

This handler can only be used with requests with a PUT method and a URL path variable named widgetId with an Int value. It is a compile time error otherwise, here is an example:

• The request doesn't have the trait ‘Method 'PUT’.

  Did you use a wrong trait type?
  For e.g., ‘PathVar "foo" Int’ instead of ‘PathVar "foo" String’?

  Or did you forget to apply an appropriate middleware?
  For e.g. The trait ‘JSONRequestBody Foo’ can be used with ‘jsonRequestBody @Foo’ middleware.

Middlewares “parse” the request and express traits such as HTTP method as a type level list. The trait attributes can be extracted from the request in a type safe manner:

type WidgetId = PathVar "widgetId" Int

putWidgetHandler :: Has WidgetId req => Handler req Widget
putWidgetHandler = Kleisli $ \request -> do
  -- wid has type Int
  let wid = get (Proxy @WidgetId) request
  ...

You cannot access the widget ID unless you have a Has WidgetId constraint in the type signature. And to satisfy this constraint, you must use the pathVar middleware which verifies the presence of such a path variable in the request.

You don’t need to know a lot of advanced haskell features to use WebGear. Using Has or Have constraints and the get function with a Proxy is enough to get you going.

Composable Handlers

Composition is a key tool to manage complexity. The ability to split code into reusable pieces and combine them at will is the foundation on which we build all our software. WebGear shines really well in this. Handlers are regular functions and you can just use function composition.

For example, if you have a number of routes that use the same basic authentication, you can extract that as a common authentication middleware.

allRoutes :: Handler req Widget
allRoutes = basicAuth "realm" checkCredentials
            $ getWidget <|> putWidget

getWidget :: Has BasicAuth req => Handler req Widget
getWidget = ...

putWidget :: Has BasicAuth req => Handler req Widget
putWidget = ...

This also shows a prominent difference between WebGear and Servant. Servant uses type level combinators to implement such functionality. This helps to derive a server, client, documentation etc from the same type definition. But composition at type level is not as easy to deal with as value level composition. Error messages are often cryptic and this is a big barrier to entry for newcomers. You also have to resort to techniques like servant-flatten to workaround problems caused by nested types.

WebGear on the other hand uses functions which can be composed trivially. The trade-off is that you cannot generate client and documentation from WebGear middlewares and handlers, they only build a server.

Building Middlewares

Often you would want to build your own traits and middlewares. This is straightforward in WebGear.

Let us take an example. Many microservices use Correlation IDs to trace a transaction across many services. This is done by sending a special HTTP header - Correlation-ID - in every request and response. All services include this correlation ID in their logs. The first request in the transaction obviously will not receive the header and should generate a random ID instead.

How do we implement this in WebGear? We start by defining a data type for correlation IDs and implement the Trait type class for it.

data CorrelationId = CorrelationId ByteString

instance MonadIO m => Trait CorrelationId Request m where
  type Attribute CorrelationId Request = CorrelationId
  type Absence CorrelationId Request = Void

  toAttribute :: Request -> m (Result CorrelationId Request)
  toAttribute req = do
    let h = requestHeader "Correlation-ID" req
    Found . CorrelationId <$> maybe randomUUID pure h

The Trait type class has two associated types - Attribute and Absence. Attribute is the type of the trait attribute and Absence is an error type for the case when correlation ID is missing. In this case, we always have a correlation ID, either from the request or a randomly generated one. So we use CorrelationId as the Attribute and Void as Absence.

The toAttribute function returns a CorrelationId either from the Correlation-ID header or generating a random UUID.

Next, let us build a middleware that uses CorrelationId:

withCorrelationId :: MonadIO m
                  => RequestMiddleware' m req (CorrelationId : req) a
withCorrelationId handler = Kleisli $ \request -> do
  result <- probe @CorrelationId request
  either absurd runHandler result
  where
    runHandler request = do
      let CorrelationId cid = get (Proxy @CorrelationId) request
      response <- runKleisli handler request
      pure $ setResponseHeader "Correlation-ID" cid response

The probe function checks the presence of CorrelationId and the result is an Either value with the Left indicating Absence and the Right indicating an Attribute.

We know that CorrelationId is always present and will not have a Left case indicating absence of the trait. But most other traits are “optional” and have to deal with a case where they are absent from the request. So probe returns an Either value to indicate this.

The absurd function from Data.Void module is used to handle the Left case that will never occur.

For the positive case where we have a correlation ID, runHandler invokes the handler and then adds the correlation ID to the response.

We can use this middleware with any of the handlers:

allRoutes :: Handler req ByteString
allRoutes = withCorrelationId
            $ putWidget <|> deleteWidget

putWidget :: Has CorrelationId req => Handler req ByteString
putWidget = method @PUT $ Kleisli $ \request -> do
  let cid = get (Proxy @CorrelationId) request
  ...

Summary

Hopefully this post has convinced you that WebGear can build type safe APIs without requiring a PhD in type theory. The user-facing parts of the library is built with care to not require many advanced Haskell concepts. You can find code, examples, and much more about WebGear in the Github repo.