HTTP APIs with WebGearSeptember 26, 2020 / webgear
I recently released WebGear - a library to build HTTP APIs. While Haskell has a number of libraries and frameworks for building HTTP API servers, WebGear is somewhat unique in the design space. I will explain some interesting features of WebGear in this post.
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.
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
Have constraints and the
get function with a
Proxy is enough to get you going.
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.
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
Trait type class has two associated types -
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
toAttribute function returns a
CorrelationId either from the
Correlation-ID header or generating a random UUID.
Next, let us build a middleware that uses
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
probe function checks the presence of
CorrelationId and the result is an
Either value with the
Absence and the
Right indicating an
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.
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 ...
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.