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:
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:
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:
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 Nat
s, 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!
mkLink
Somewhere between the 0.2 release and now, mkLink
got a whole lot better (thanks Christian Marie!). mkLink
makes urls that are statically guaranteed to belong to your API, without any Template Haskell. Combined with response headers, you can now easily create, for instance, type-safe redirect headers. Combined with the new HTML support, you can easily make links that you know will not 404.
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
To
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:
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.