servant 0.4 released

Table of contents

Since the last major release, a lot happened in and around servant. Definitely enough to justify a new release. This post announces new releases of all the servant packages, with many local changes but also some major ones that affect all packages. You can find the detailed changelogs at the end of this post, but here are a few major features you may want to learn about. This website also features a new tutorial that explains how to use servant from scratch.

Multiple content-type support

servant combinators are not JSON-centric anymore.

If you had an API type like the following with servant 0.2.x:

type API = -- list users
           "users" :> Get [User]
           -- update an user
      :<|> "user" :> Capture "username" Text :> ReqBody User :> Put ()

You now have to change it to:

type API = -- list users
           "users" :> Get '[JSON] [User]
      :<|> "user" :> Capture "username" Text :> ReqBody '[JSON] User :> Put '[JSON] ()

Wherever applicable (i.e., ReqBody and all the combinators that correspond to an HTTP method), you can now specify all the content types in which you want to want to be able to encode/decode values. As you can see, we use the DataKinds GHC extension to let you specify a type-level list of content-types, which are simple dummy types:

data JSON

In servant-server, a list of these content-types as the first argument of a method gets translated into a set of constraints on the return type:

Get '[JSON, PlainText] Int
==>
MimeRender JSON Int, MimeRender PlainText Int => EitherT ServantErr IO Int

Which have unsurprising instances:

instance (ToJSON a) => MimeRender JSON a

Thus, servant checks at compile-time that it really can serialize your values as you describe. And of course, it handles picking the appropriate serialization format based on the request’s “Accept” header for you.

(For ReqBody, deserialization is involved. For servant-client, the logic goes the other way around - serialization for ReqBody, deserialization for methods.)

servant-blaze and servant-lucid

Declaring new content-types, and the associated constraints for them, is quite easy. But to make it easier still, we are also announcing two new packages: servant-blaze and servant-lucid. To use them, just import their HTML datatype:

import Servant.HTML.Lucid (HTML) -- or Servant.HTML.Blaze

type MyHTML = Get '[HTML] User

And User will be checked for the appropriate (e.g. ToHtml) instance.

Response headers

There was no easy way so far to have handlers add headers to a response. We’ve since come up with a solution that stays true to the servant spirit: what headers your response will include (and what their types are) is still enforced statically:

type MyHandler = Get '[JSON] (Headers '[Header "Location" Link] User)

myHandler :: Server MyHandler
myHandler = return $ addHeader <someLink> $ <someuser>

servant-docs and servant-client are also response-header aware.

Our current solution isn’t something we are entirely happy with from an internal persepctive. We use overlapping instances for all the handlers, which some might think is already a problem. But more concretely, there’s the threat of an exponential blowup in the number of instances we have to declare. And that can be a problem for end users too, if they decide to further modify behavior via a similar mechanism. But these things thankfully don’t seem to pose any immediate problems.

Running handlers in other monads than EitherT

An often-requested feature has been easy use of datatypes/monads besides EitherT. Now we believe we have a good story for that (thanks in large part to rschatz). To convert from one datatype to another, all you need to do is provide a natural transformation between them. For example:

type ReaderAPI = "a" :> Get '[JSON] Int
            :<|> "b" :> Get '[JSON] String

readerServerT :: ServerT ReaderAPI (Reader String)
readerServerT = return 1797 :<|> ask

readerServer :: Server ReaderAPI
readerServer = enter (Nat $ return . (`runReader` "hi")) readerServerT

The new ServerT type synonym takes an extra paramer that represents what datatype/monad you are using over your handlers (instead of EitherT ServantErr IO).

(Note that we also provide a number of pre-existing Nats, which are an instance of Category. We could have used

readerServer = enter (generalizeNat . (runReaderTNat "hi")) readerServerT

instead (with . being from Control.Category).)

Note that the datatypes you can use now don’t even need to be monads!

Left

We also changed the default type of handlers from EitherT (Int,String) IO a to EitherT ServantErr IO a. Now it is possible to return headers and a response body in the Left case.

We also now export function errXXX (where XXX is a 300-599 HTTP status code) with sensible reason strings.

BaseUrl

We also changed the client function from servant-client so that, instead of returning various functions that each take a BaseUrl argument (often in inconvenient argument positions), the client function itself takes a BaseUrl argument, and the functions it returns don’t. So the type of client went from

client :: HasClient (Canonicalize layout) => Proxy layout -> Client layout

To

client :: HasClient (Canonicalize layout) => Proxy layout -> BaseUrl -> Client layout

Complete CHANGELOGs

Website

We also decided to switch to hakyll in order to be able to have a blog as well as some static pages that collect tips and tricks that people have found. We also used this opportunity to rewrite the getting started into a more informative tutorial, now available here.

Conclusions

As you can see, more and more information is getting encoded statically - the types are becoming a pretty rich DSL. In order to keep the noise down, do what you normally do: abstract away common patterns! If your endpoints always return the same content-types, make aliases:

type Get' a = Get '[JSON, HTML, PlainText] a

There’s still an outstanding issue with the errors servant returns when a request doesn’t get handled. For example, if the path of a request, but not the method nor the request body, match, rather than returning a 405 (Method Not Allowed) we return a 400 (Bad Request), which is not the desired behavior. Andres Löh made some great suggestions for how to improve our routing time complexity, and hopefully we can integrate a fix for this issue when we tackle that.

We also merged our repos into servant. Please use that repo exclusively for PRs and issues (we’ll get rid of the others eventually).

Special thanks to the Anchor team from Australia, Matthew Pickering, Daniel Larsson, Phil Freeman, Matthias Fischmann, rschatz, Mateusz Kowalczyk, Brandon Martin and Sean Leather who’ve contributed from little fixes to whole new features. Several companies are now running servant-powered web applications.

Posted on May 10, 2015 by The servant team