39 KiB
Serving an API
Enough chit-chat about type-level combinators and representing an API as a type. Can we have a webservice already?
A first example
Equipped with some basic knowledge about the way we represent APIs, let's now write our first webservice.
Setting up our project
So that you don't have to guess too much on the Cabal file, here's a minimal example file. You can run the main function provided in the following examples within the Server
module without using the separate executable, but these are provided if you wanted to split that out. You can get a layout that is most of the way to what you want with stack new servant-tutorial new-template
. Here's the Cabal file first:
-- servant-tutorial.cabal
name: servant-tutorial
version: 0.1.0.0
synopsis: Initial project template from stack
description: Please see README.md
license: BSD3
license-file: LICENSE
author: You!
maintainer: allyou@yourplace.com
copyright: 2016, Still You!
category: Web
build-type: Simple
cabal-version: >=1.10
library
hs-source-dirs: src
exposed-modules: Server
build-depends: base >= 4.7 && < 5
, base-compat
, aeson >= 0.11 && < 0.12
, attoparsec
, blaze-html
, blaze-markup
, bytestring
, directory
, http-media
, lucid
, mtl
, servant >= 0.5 && 0.6
, servant-server
, string-conversions
, text
, time
, wai
, warp
default-language: Haskell2010
executable servant-tutorial
hs-source-dirs: app
main-is: Main.hs
ghc-options: -threaded -rtsopts -with-rtsopts=-N
build-depends: base
, servant-tutorial
, warp
default-language: Haskell2010
If you're using Stack, then in your stack.yaml
you may want to specify:
extra-deps:
- aeson-0.11.1.1
- servant-0.5
- servant-server-0.5
Then the app/Main.hs
file, so you can produce a binary for running locally:
-- app/Main.hs
module Main where
import Server (app)
import Network.Wai.Handler.Warp (run)
main :: IO ()
main = run 8081 app
Note that this will run whatever app is named app
. The final result of constructing a Servant server is a WAI app which can be run directly with Warp's runner.
Writing our Server
The source for this tutorial section is a literate haskell file. To start our Server module, we need to have some language extensions and imports:
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeOperators #-}
module Server where
import Prelude ()
import Prelude.Compat
import Control.Monad.Except
import Control.Monad.Reader
import Data.Aeson.Compat
import Data.Aeson.Types
import Data.Attoparsec.ByteString
import Data.ByteString (ByteString)
import Data.List
import Data.Maybe
import Data.String.Conversions
import Data.Time.Calendar
import GHC.Generics
import Lucid
import Network.HTTP.Media ((//), (/:))
import Network.Wai
import Network.Wai.Handler.Warp
import Servant
import System.Directory
import Text.Blaze
import Text.Blaze.Html.Renderer.Utf8
import qualified Data.Aeson.Parser
import qualified Text.Blaze.Html
Important: the Servant
module comes from the servant-server package,
the one that lets us run webservers that implement a particular API type. It
reexports all the types from the servant package that let you declare API
types as well as everything you need to turn your request handlers into a
fully-fledged webserver. This means that in your applications, you can just add
servant-server as a dependency, import Servant
and not worry about anything
else.
We will write a server that will serve the following API.
type UserAPI1 = "users" :> Get '[JSON] [User]
Here's what we would like to see when making a GET request to /users
.
[ {"name": "Isaac Newton", "age": 372, "email": "isaac@newton.co.uk", "registration_date": "1683-03-01"}
, {"name": "Albert Einstein", "age": 136, "email": "ae@mc2.org", "registration_date": "1905-12-01"}
]
Now let's define our User
data type and write some instances for it.
data User = User
{ name :: String
, age :: Int
, email :: String
, registration_date :: Day
} deriving (Eq, Show, Generic)
instance ToJSON User
Nothing funny going on here. But we now can define our list of two users.
users1 :: [User]
users1 =
[ User "Isaac Newton" 372 "isaac@newton.co.uk" (fromGregorian 1683 3 1)
, User "Albert Einstein" 136 "ae@mc2.org" (fromGregorian 1905 12 1)
]
Let's also write our API type.
type UserAPI1 = "users" :> Get '[JSON] [User]
We can now take care of writing the actual webservice that will handle requests
to such an API. This one will be very simple, being reduced to just a single
endpoint. The type of the web application is determined by the API type,
through a type family named Server
. (Type families are just functions that
take types as input and return types.) The Server
type family will compute
the right type that a bunch of request handlers should have just from the
corresponding API type.
The first thing to know about the Server
type family is that behind the
scenes it will drive the routing, letting you focus only on the business
logic. The second thing to know is that for each endpoint, your handlers will
by default run in the ExceptT ServantErr IO
monad. This is overridable very
easily, as explained near the end of this guide. Third thing, the type of the
value returned in that monad must be the same as the second argument of the
HTTP method combinator used for the corresponding endpoint. In our case, it
means we must provide a handler of type ExceptT ServantErr IO [User]
. Well,
we have a monad, let's just return
our list:
server1 :: Server UserAPI1
server1 = return users1
That's it. Now we can turn server
into an actual webserver using
wai and
warp:
userAPI :: Proxy UserAPI1
userAPI = Proxy
-- 'serve' comes from servant and hands you a WAI Application,
-- which you can think of as an "abstract" web application,
-- not yet a webserver.
app1 :: Application
app1 = serve userAPI server1
The userAPI
bit is, alas, boilerplate (we need it to guide type inference).
But that's about as much boilerplate as you get.
And we're done! Let's run our webservice on the port 8081.
main :: IO ()
main = run 8081 app1
You can put this all into a file or just grab servant's repo and look at the doc/tutorial directory. This code (the source of this web page) is in doc/tutorial/Server.md.
If you run it, you can go to http://localhost:8081/users
in your browser or
query it with curl and you see:
$ curl http://localhost:8081/users
[{"email":"isaac@newton.co.uk","registration_date":"1683-03-01","age":372,"name":"Isaac Newton"},{"email":"ae@mc2.org","registration_date":"1905-12-01","age":136,"name":"Albert Einstein"}]
More endpoints
What if we want more than one endpoint? Let's add /albert
and /isaac
to
view the corresponding users encoded in JSON.
type UserAPI2 = "users" :> Get '[JSON] [User]
:<|> "albert" :> Get '[JSON] User
:<|> "isaac" :> Get '[JSON] User
And let's adapt our code a bit.
isaac :: User
isaac = User "Isaac Newton" 372 "isaac@newton.co.uk" (fromGregorian 1683 3 1)
albert :: User
albert = User "Albert Einstein" 136 "ae@mc2.org" (fromGregorian 1905 12 1)
users2 :: [User]
users2 = [isaac, albert]
Now, just like we separate the various endpoints in UserAPI
with :<|>
, we
are going to separate the handlers with :<|>
too! They must be provided in
the same order as in in the API type.
server2 :: Server UserAPI2
server2 = return users2
:<|> return albert
:<|> return isaac
And that's it! You can run this example in the same way that we showed for
server1
and check out the data available at /users
, /albert
and /isaac
.
From combinators to handler arguments
Fine, we can write trivial webservices easily, but none of the two above use
any "fancy" combinator from servant. Let's address this and use QueryParam
,
Capture
and ReqBody
right away. You'll see how each occurence of these
combinators in an endpoint makes the corresponding handler receive an
argument of the appropriate type automatically. You don't have to worry about
manually looking up URL captures or query string parameters, or
decoding/encoding data from/to JSON. Never.
We are going to use the following data types and functions to implement a
server for API
.
type API = "position" :> Capture "x" Int :> Capture "y" Int :> Get '[JSON] Position
:<|> "hello" :> QueryParam "name" String :> Get '[JSON] HelloMessage
:<|> "marketing" :> ReqBody '[JSON] ClientInfo :> Post '[JSON] Email
data Position = Position
{ xCoord :: Int
, yCoord :: Int
} deriving Generic
instance ToJSON Position
newtype HelloMessage = HelloMessage { msg :: String }
deriving Generic
instance ToJSON HelloMessage
data ClientInfo = ClientInfo
{ clientName :: String
, clientEmail :: String
, clientAge :: Int
, clientInterestedIn :: [String]
} deriving Generic
instance FromJSON ClientInfo
instance ToJSON ClientInfo
data Email = Email
{ from :: String
, to :: String
, subject :: String
, body :: String
} deriving Generic
instance ToJSON Email
emailForClient :: ClientInfo -> Email
emailForClient c = Email from' to' subject' body'
where from' = "great@company.com"
to' = clientEmail c
subject' = "Hey " ++ clientName c ++ ", we miss you!"
body' = "Hi " ++ clientName c ++ ",\n\n"
++ "Since you've recently turned " ++ show (clientAge c)
++ ", have you checked out our latest "
++ intercalate ", " (clientInterestedIn c)
++ " products? Give us a visit!"
We can implement handlers for the three endpoints:
server3 :: Server API
server3 = position
:<|> hello
:<|> marketing
where position :: Int -> Int -> ExceptT ServantErr IO Position
position x y = return (Position x y)
hello :: Maybe String -> ExceptT ServantErr IO HelloMessage
hello mname = return . HelloMessage $ case mname of
Nothing -> "Hello, anonymous coward"
Just n -> "Hello, " ++ n
marketing :: ClientInfo -> ExceptT ServantErr IO Email
marketing clientinfo = return (emailForClient clientinfo)
Did you see that? The types for your handlers changed to be just what we needed! In particular:
-
a
Capture "something" a
becomes an argument of typea
(forposition
); -
a
QueryParam "something" a
becomes an argument of typeMaybe a
(because an endpoint can technically be accessed without specifying any query string parameter, we decided to "force" handlers to be aware that the parameter might not always be there); -
a
ReqBody contentTypeList a
becomes an argument of typea
;
And that's it. Here's the example in action:
$ curl http://localhost:8081/position/1/2
{"xCoord":1,"yCoord":2}
$ curl http://localhost:8081/hello
{"msg":"Hello, anonymous coward"}
$ curl http://localhost:8081/hello?name=Alp
{"msg":"Hello, Alp"}
$ curl -X POST -d '{"clientName":"Alp Mestanogullari", "clientEmail" : "alp@foo.com", "clientAge": 25, "clientInterestedIn": ["haskell", "mathematics"]}' -H 'Accept: application/json' -H 'Content-type: application/json' http://localhost:8081/marketing
{"subject":"Hey Alp Mestanogullari, we miss you!","body":"Hi Alp Mestanogullari,\n\nSince you've recently turned 25, have you checked out our latest haskell, mathematics products? Give us a visit!","to":"alp@foo.com","from":"great@company.com"}
For reference, here's a list of some combinators from servant:
Delete
,Get
,Patch
,Post
,Put
: these do not become arguments. They provide the return type of handlers, which usually isExceptT ServantErr IO <something>
.Capture "something" a
becomes an argument of typea
.QueryParam "something" a
,Header "something" a
all become arguments of typeMaybe a
, because there might be no value at all specified by the client for these.QueryFlag "something"
gets turned into an argument of typeBool
.QueryParams "something" a
gets turned into an argument of type[a]
.ReqBody contentTypes a
gets turned into an argument of typea
.
The FromHttpApiData
/ToHttpApiData
classes
Wait... How does servant know how to decode the Int
s from the URL? Or how
to decode a ClientInfo
value from the request body? This is what this and the
following two sections address.
Capture
s and QueryParam
s are represented by some textual value in URLs.
Header
s are similarly represented by a pair of a header name and a
corresponding (textual) value in the request's "metadata". How types are
decoded from headers, captures, and query params is expressed in a class
FromHttpApiData
(from the package
http-api-data):
class FromHttpApiData a where
{-# MINIMAL parseUrlPiece | parseQueryParam #-}
-- | Parse URL path piece.
parseUrlPiece :: Text -> Either Text a
parseUrlPiece = parseQueryParam
-- | Parse HTTP header value.
parseHeader :: ByteString -> Either Text a
parseHeader = parseUrlPiece . decodeUtf8
-- | Parse query param value.
parseQueryParam :: Text -> Either Text a
parseQueryParam = parseUrlPiece
As you can see, as long as you provide either parseUrlPiece
(for Capture
s)
or parseQueryParam
(for QueryParam
s), the other methods will be defined in
terms of this.
http-api-data provides a decent number of instances, helpers for defining new ones, and wonderful documentation.
There's not much else to say about these classes. You will need instances for
them when using Capture
, QueryParam
, QueryParams
, and Header
with your
types. You will need FromHttpApiData
instances for server-side request
handlers and ToHttpApiData
instances only when using
servant-client, as described in the section about deriving haskell
functions to query an API.
Using content-types with your data types
The same principle was operating when decoding request bodies from JSON, and responses into JSON. (JSON is just the running example - you can do this with any content-type.)
This section introduces a couple of typeclasses provided by servant that make all of this work.
The truth behind JSON
What exactly is JSON
(the type as used in Get '[JSON] User
)? Like the 3
other content-types provided out of the box by servant, it's a really dumb
data type.
data JSON
data PlainText
data FormUrlEncoded
data OctetStream
Obviously, this is not all there is to JSON
, otherwise it would be quite
pointless. Like most of the data types in servant, JSON
is mostly there as
a special symbol that's associated with encoding (resp. decoding) to (resp.
from) the JSON format. The way this association is performed can be
decomposed into two steps.
The first step is to provide a proper
MediaType
(from
http-media)
representation for JSON
, or for your own content-types. If you look at the
haddocks from this link, you can see that we just have to specify
application/json
using the appropriate functions. In our case, we can just
use (//) :: ByteString -> ByteString -> MediaType
. The precise way to specify
the MediaType
is to write an instance for the Accept
class:
-- for reference:
class Accept ctype where
contentType :: Proxy ctype -> MediaType
instance Accept JSON where
contentType _ = "application" // "json"
The second step is centered around the MimeRender
and MimeUnrender
classes.
These classes just let you specify a way to encode and decode
values into or from your content-type's representation.
class Accept ctype => MimeRender ctype a where
mimeRender :: Proxy ctype -> a -> ByteString
-- alternatively readable as:
mimeRender :: Proxy ctype -> (a -> ByteString)
Given a content-type and some user type, MimeRender
provides a function that
encodes values of type a
to lazy ByteString
s.
In the case of JSON
, this is easily dealt with! For any type a
with a
ToJSON
instance, we can render values of that type to JSON using
Data.Aeson.encode
.
instance ToJSON a => MimeRender JSON a where
mimeRender _ = encode
And now the MimeUnrender
class, which lets us extract values from lazy
ByteString
s, alternatively failing with an error string.
class Accept ctype => MimeUnrender ctype a where
mimeUnrender :: Proxy ctype -> ByteString -> Either String a
We don't have much work to do there either, Data.Aeson.eitherDecode
is
precisely what we need. However, it only allows arrays and objects as toplevel
JSON values and this has proven to get in our way more than help us so we wrote
our own little function around aeson and attoparsec that allows any type of
JSON value at the toplevel of a "JSON document". Here's the definition in case
you are curious.
eitherDecodeLenient :: FromJSON a => ByteString -> Either String a
eitherDecodeLenient input = do
v :: Value <- parseOnly (Data.Aeson.Parser.value <* endOfInput) (cs input)
parseEither parseJSON v
This function is exactly what we need for our MimeUnrender
instance.
instance FromJSON a => MimeUnrender JSON a where
mimeUnrender _ = eitherDecodeLenient
And this is all the code that lets you use JSON
with ReqBody
, Get
,
Post
and friends. We can check our understanding by implementing support
for an HTML
content-type, so that users of your webservice can access an
HTML representation of the data they want, ready to be included in any HTML
document, e.g. using jQuery's load
function,
simply by adding Accept: text/html
to their request headers.
Case-studies: servant-blaze and servant-lucid
These days, most of the haskellers who write their HTML UIs directly from Haskell use either blaze-html or lucid. The best option for servant is obviously to support both (and hopefully other templating solutions!). We're first going to look at lucid:
data HTMLLucid
Once again, the data type is just there as a symbol for the encoding/decoding functions, except that this time we will only worry about encoding since lucid doesn't provide a way to extract data from HTML.
instance Accept HTMLLucid where
contentType _ = "text" // "html" /: ("charset", "utf-8")
Note that this instance uses the (/:)
operator from http-media which lets
us specify additional information about a content-type, like the charset here.
The rendering instances call similar functions that take
types with an appropriate instance to an "abstract" HTML representation and
then write that to a ByteString
.
instance ToHtml a => MimeRender HTMLLucid a where
mimeRender _ = renderBS . toHtml
-- let's also provide an instance for lucid's
-- 'Html' wrapper.
instance MimeRender HTMLLucid (Html a) where
mimeRender _ = renderBS
For blaze-html everything works very similarly:
-- For this tutorial to compile 'HTMLLucid' and 'HTMLBlaze' have to be
-- distinct. Usually you would stick to one html rendering library and then
-- you can go with one 'HTML' type.
data HTMLBlaze
instance Accept HTMLBlaze where
contentType _ = "text" // "html" /: ("charset", "utf-8")
instance ToMarkup a => MimeRender HTMLBlaze a where
mimeRender _ = renderHtml . Text.Blaze.Html.toHtml
-- while we're at it, just like for lucid we can
-- provide an instance for rendering blaze's 'Html' type
instance MimeRender HTMLBlaze Text.Blaze.Html.Html where
mimeRender _ = renderHtml
Both servant-blaze and
servant-lucid let you use
HTMLLucid
and HTMLBlaze
in any content-type list as long as you provide an instance of the
appropriate class (ToMarkup
for blaze-html, ToHtml
for lucid).
We can now write a webservice that uses servant-lucid to show the HTMLLucid
content-type in action. We will be serving the following API:
type PersonAPI = "persons" :> Get '[JSON, HTMLLucid] [Person]
where Person
is defined as follows:
data Person = Person
{ firstName :: String
, lastName :: String
} deriving Generic -- for the JSON instance
instance ToJSON Person
Now, let's teach lucid how to render a Person
as a row in a table, and then
a list of Person
s as a table with a row per person.
-- HTML serialization of a single person
instance ToHtml Person where
toHtml person =
tr_ $ do
td_ (toHtml $ firstName person)
td_ (toHtml $ lastName person)
-- do not worry too much about this
toHtmlRaw = toHtml
-- HTML serialization of a list of persons
instance ToHtml [Person] where
toHtml persons = table_ $ do
tr_ $ do
th_ "first name"
th_ "last name"
-- this just calls toHtml on each person of the list
-- and concatenates the resulting pieces of HTML together
foldMap toHtml persons
toHtmlRaw = toHtml
We create some Person
values and serve them as a list:
people :: [Person]
people =
[ Person "Isaac" "Newton"
, Person "Albert" "Einstein"
]
personAPI :: Proxy PersonAPI
personAPI = Proxy
server4 :: Server PersonAPI
server4 = return people
app2 :: Application
app2 = serve personAPI server4
And we're good to go:
$ curl http://localhost:8081/persons
[{"lastName":"Newton","firstName":"Isaac"},{"lastName":"Einstein","firstName":"Albert"}]
$ curl -H 'Accept: text/html' http://localhost:8081/persons
<table><tr><td>first name</td><td>last name</td></tr><tr><td>Isaac</td><td>Newton</td></tr><tr><td>Albert</td><td>Einstein</td></tr></table>
# or just point your browser to http://localhost:8081/persons
The ExceptT ServantErr IO
monad
At the heart of the handlers is the monad they run in, namely ExceptT ServantErr IO
(haddock documentation for ExceptT
).
One might wonder: why this monad? The answer is that it is the
simplest monad with the following properties:
- it lets us both return a successful result (using
return
) or "fail" with a descriptive error (usingthrowError
); - it lets us perform IO, which is absolutely vital since most webservices exist
as interfaces to databases that we interact with in
IO
.
Let's recall some definitions.
-- from the 'mtl' package at
newtype ExceptT e m a = ExceptT (m (Either e a))
In short, this means that a handler of type ExceptT ServantErr IO a
is simply
equivalent to a computation of type IO (Either ServantErr a)
, that is, an IO
action that either returns an error or a result.
The module Control.Monad.Except
from which ExceptT
comes is worth looking at.
Perhaps most importantly, ExceptT
is an instance of MonadError
, so
throwError
can be used to return an error from your handler (whereas return
is enough to return a success).
Most of what you'll be doing in your handlers is running some IO and, depending on the result, you might sometimes want to throw an error of some kind and abort early. The next two sections cover how to do just that.
Performing IO
Another important instance from the list above is MonadIO m => MonadIO (ExceptT e m)
.
MonadIO
is a class from the transformers package defined as:
class Monad m => MonadIO m where
liftIO :: IO a -> m a
The IO
monad provides a MonadIO
instance. Hence for any type
e
, ExceptT e IO
has a MonadIO
instance. So if you want to run any kind of
IO computation in your handlers, just use liftIO
:
type IOAPI1 = "myfile.txt" :> Get '[JSON] FileContent
newtype FileContent = FileContent
{ content :: String }
deriving Generic
instance ToJSON FileContent
server5 :: Server IOAPI1
server5 = do
filecontent <- liftIO (readFile "myfile.txt")
return (FileContent filecontent)
Failing, through ServantErr
If you want to explicitly fail at providing the result promised by an endpoint
using the appropriate HTTP status code (not found, unauthorized, etc) and some
error message, all you have to do is use the throwError
function mentioned above
and provide it with the appropriate value of type ServantErr
, which is
defined as:
data ServantErr = ServantErr
{ errHTTPCode :: Int
, errReasonPhrase :: String
, errBody :: ByteString -- lazy bytestring
, errHeaders :: [Header]
}
Many standard values are provided out of the box by the Servant.Server
module. If you want to use these values but add a body or some headers, just
use record update syntax:
failingHandler :: ExceptT ServantErr IO ()
failingHandler = throwError myerr
where myerr :: ServantErr
myerr = err503 { errBody = "Sorry dear user." }
Here's an example where we return a customised 404-Not-Found error message in the response body if "myfile.txt" isn't there:
server6 :: Server IOAPI1
server6 = do
exists <- liftIO (doesFileExist "myfile.txt")
if exists
then liftIO (readFile "myfile.txt") >>= return . FileContent
else throwError custom404Err
where custom404Err = err404 { errBody = "myfile.txt just isn't there, please leave this server alone." }
Here's how that server looks in action:
$ curl --verbose http://localhost:8081/myfile.txt
[snip]
* Connected to localhost (127.0.0.1) port 8081 (#0)
> GET /myfile.txt HTTP/1.1
> User-Agent: curl/7.30.0
> Host: localhost:8081
> Accept: */*
>
< HTTP/1.1 404 Not Found
[snip]
myfile.txt just isnt there, please leave this server alone.
$ echo Hello > myfile.txt
$ curl --verbose http://localhost:8081/myfile.txt
[snip]
* Connected to localhost (127.0.0.1) port 8081 (#0)
> GET /myfile.txt HTTP/1.1
> User-Agent: curl/7.30.0
> Host: localhost:8081
> Accept: */*
>
< HTTP/1.1 200 OK
[snip]
< Content-Type: application/json
[snip]
{"content":"Hello\n"}
Response headers
To add headers to your response, use addHeader. Note that this changes the type of your API, as we can see in the following example:
type MyHandler = Get '[JSON] (Headers '[Header "X-An-Int" Int] User)
myHandler :: Server MyHandler
myHandler = return $ addHeader 1797 albert
Note that the type of addHeader x
is different than the type of x
!
Serving static files
servant-server also provides a way to just serve the content of a directory
under some path in your web API. As mentioned earlier in this document, the
Raw
combinator can be used in your APIs to mean "plug here any WAI
application". Well, servant-server provides a function to get a file and
directory serving WAI application, namely:
-- exported by Servant and Servant.Server
serveDirectory :: FilePath -> Server Raw
serveDirectory
's argument must be a path to a valid directory.
Here's an example API that will serve some static files:
type StaticAPI = "static" :> Raw
And the server:
staticAPI :: Proxy StaticAPI
staticAPI = Proxy
server7 :: Server StaticAPI
server7 = serveDirectory "static-files"
app3 :: Application
app3 = serve staticAPI server7
This server will match any request whose path starts with /static
and will look
for a file at the path described by the rest of the request path, inside the
static-files/ directory of the path you run the program from.
In other words: If a client requests /static/foo.txt
, the server will look for a file at
./static-files/foo.txt
. If that file exists it'll succeed and serve the file.
If it doesn't exist, the handler will fail with a 404
status code.
Nested APIs
Let's see how you can define APIs in a modular way, while avoiding repetition. Consider this simple example:
type UserAPI3 = -- view the user with given userid, in JSON
Capture "userid" Int :> Get '[JSON] User
:<|> -- delete the user with given userid. empty response
Capture "userid" Int :> Delete '[] ()
We can instead factor out the userid
:
type UserAPI4 = Capture "userid" Int :>
( Get '[JSON] User
:<|> Delete '[] ()
)
However, you have to be aware that this has an effect on the type of the
corresponding Server
:
Server UserAPI3 = (Int -> ExceptT ServantErr IO User)
:<|> (Int -> ExceptT ServantErr IO ())
Server UserAPI4 = Int -> ( ExceptT ServantErr IO User
:<|> ExceptT ServantErr IO ()
)
In the first case, each handler receives the userid argument. In the latter,
the whole Server
takes the userid and has handlers that are just
computations in ExceptT
, with no arguments. In other words:
server8 :: Server UserAPI3
server8 = getUser :<|> deleteUser
where getUser :: Int -> ExceptT ServantErr IO User
getUser _userid = error "..."
deleteUser :: Int -> ExceptT ServantErr IO ()
deleteUser _userid = error "..."
-- notice how getUser and deleteUser
-- have a different type! no argument anymore,
-- the argument directly goes to the whole Server
server9 :: Server UserAPI4
server9 userid = getUser userid :<|> deleteUser userid
where getUser :: Int -> ExceptT ServantErr IO User
getUser = error "..."
deleteUser :: Int -> ExceptT ServantErr IO ()
deleteUser = error "..."
Note that there's nothing special about Capture
that lets you "factor it
out": this can be done with any combinator. Here are a few examples of APIs
with a combinator factored out for which we can write a perfectly valid
Server
.
-- we just factor out the "users" path fragment
type API1 = "users" :>
( Get '[JSON] [User] -- user listing
:<|> Capture "userid" Int :> Get '[JSON] User -- view a particular user
)
-- we factor out the Request Body
type API2 = ReqBody '[JSON] User :>
( Get '[JSON] User -- just display the same user back, don't register it
:<|> Post '[JSON] () -- register the user. empty response
)
-- we factor out a Header
type API3 = Header "Authorization" Token :>
( Get '[JSON] SecretData -- get some secret data, if authorized
:<|> ReqBody '[JSON] SecretData :> Post '[] () -- add some secret data, if authorized
)
newtype Token = Token ByteString
newtype SecretData = SecretData ByteString
This approach lets you define APIs modularly and assemble them all into one big API type only at the end.
type UsersAPI =
Get '[JSON] [User] -- list users
:<|> ReqBody '[JSON] User :> Post '[] () -- add a user
:<|> Capture "userid" Int :>
( Get '[JSON] User -- view a user
:<|> ReqBody '[JSON] User :> Put '[] () -- update a user
:<|> Delete '[] () -- delete a user
)
usersServer :: Server UsersAPI
usersServer = getUsers :<|> newUser :<|> userOperations
where getUsers :: ExceptT ServantErr IO [User]
getUsers = error "..."
newUser :: User -> ExceptT ServantErr IO ()
newUser = error "..."
userOperations userid =
viewUser userid :<|> updateUser userid :<|> deleteUser userid
where
viewUser :: Int -> ExceptT ServantErr IO User
viewUser = error "..."
updateUser :: Int -> User -> ExceptT ServantErr IO ()
updateUser = error "..."
deleteUser :: Int -> ExceptT ServantErr IO ()
deleteUser = error "..."
type ProductsAPI =
Get '[JSON] [Product] -- list products
:<|> ReqBody '[JSON] Product :> Post '[] () -- add a product
:<|> Capture "productid" Int :>
( Get '[JSON] Product -- view a product
:<|> ReqBody '[JSON] Product :> Put '[] () -- update a product
:<|> Delete '[] () -- delete a product
)
data Product = Product { productId :: Int }
productsServer :: Server ProductsAPI
productsServer = getProducts :<|> newProduct :<|> productOperations
where getProducts :: ExceptT ServantErr IO [Product]
getProducts = error "..."
newProduct :: Product -> ExceptT ServantErr IO ()
newProduct = error "..."
productOperations productid =
viewProduct productid :<|> updateProduct productid :<|> deleteProduct productid
where
viewProduct :: Int -> ExceptT ServantErr IO Product
viewProduct = error "..."
updateProduct :: Int -> Product -> ExceptT ServantErr IO ()
updateProduct = error "..."
deleteProduct :: Int -> ExceptT ServantErr IO ()
deleteProduct = error "..."
type CombinedAPI = "users" :> UsersAPI
:<|> "products" :> ProductsAPI
server10 :: Server CombinedAPI
server10 = usersServer :<|> productsServer
Finally, we can realize the user and product APIs are quite similar and abstract that away:
-- API for values of type 'a'
-- indexed by values of type 'i'
type APIFor a i =
Get '[JSON] [a] -- list 'a's
:<|> ReqBody '[JSON] a :> Post '[] () -- add an 'a'
:<|> Capture "id" i :>
( Get '[JSON] a -- view an 'a' given its "identifier" of type 'i'
:<|> ReqBody '[JSON] a :> Put '[] () -- update an 'a'
:<|> Delete '[] () -- delete an 'a'
)
-- Build the appropriate 'Server'
-- given the handlers of the right type.
serverFor :: ExceptT ServantErr IO [a] -- handler for listing of 'a's
-> (a -> ExceptT ServantErr IO ()) -- handler for adding an 'a'
-> (i -> ExceptT ServantErr IO a) -- handler for viewing an 'a' given its identifier of type 'i'
-> (i -> a -> ExceptT ServantErr IO ()) -- updating an 'a' with given id
-> (i -> ExceptT ServantErr IO ()) -- deleting an 'a' given its id
-> Server (APIFor a i)
serverFor = error "..."
-- implementation left as an exercise. contact us on IRC
-- or the mailing list if you get stuck!
Using another monad for your handlers
Remember how Server
turns combinators for HTTP methods into ExceptT ServantErr IO
? Well, actually, there's more to that. Server
is actually a
simple type synonym.
type Server api = ServerT api (ExceptT ServantErr IO)
ServerT
is the actual type family that computes the required types for the
handlers that's part of the HasServer
class. It's like Server
except that
it takes another parameter which is the monad you want your handlers to run in,
or more generally the return types of your handlers. This third parameter is
used for specifying the return type of the handler for an endpoint, e.g when
computing ServerT (Get '[JSON] Person) SomeMonad
. The result would be
SomeMonad Person
.
The first and main question one might have then is: how do we write handlers that run in another monad? How can we "bring back" the value from a given monad into something servant can understand?
Natural transformations
If we have a function that gets us from an m a
to an n a
, for any a
, what
do we have?
newtype m :~> n = Nat { unNat :: forall a. m a -> n a}
For example:
listToMaybeNat :: [] :~> Maybe
listToMaybeNat = Nat listToMaybe -- from Data.Maybe
(Nat
comes from "natural transformation", in case you're wondering.)
So if you want to write handlers using another monad/type than ExceptT ServantErr IO
, say the Reader String
monad, the first thing you have to
prepare is a function:
readerToHandler :: Reader String :~> ExceptT ServantErr IO
Let's start with readerToHandler'
. We obviously have to run the Reader
computation by supplying it with a String
, like "hi"
. We get an a
out
from that and can then just return
it into ExceptT
. We can then just wrap
that function with the Nat
constructor to make it have the fancier type.
readerToHandler' :: forall a. Reader String a -> ExceptT ServantErr IO a
readerToHandler' r = return (runReader r "hi")
readerToHandler :: Reader String :~> ExceptT ServantErr IO
readerToHandler = Nat readerToHandler'
We can write some simple webservice with the handlers running in Reader String
.
type ReaderAPI = "a" :> Get '[JSON] Int
:<|> "b" :> Get '[JSON] String
readerAPI :: Proxy ReaderAPI
readerAPI = Proxy
readerServerT :: ServerT ReaderAPI (Reader String)
readerServerT = a :<|> b
where a :: Reader String Int
a = return 1797
b :: Reader String String
b = ask
We unfortunately can't use readerServerT
as an argument of serve
, because
serve
wants a Server ReaderAPI
, i.e., with handlers running in ExceptT ServantErr IO
. But there's a simple solution to this.
Enter enter
That's right. We have just written readerToHandler
, which is exactly what we
would need to apply to all handlers to make the handlers have the
right type for serve
. Being cumbersome to do by hand, we provide a function
enter
which takes a natural transformation between two parametrized types m
and n
and a ServerT someapi m
, and returns a ServerT someapi n
.
In our case, we can wrap up our little webservice by using enter readerToHandler
on our handlers.
readerServer :: Server ReaderAPI
readerServer = enter readerToHandler readerServerT
app4 :: Application
app4 = serve readerAPI readerServer
This is the webservice in action:
$ curl http://localhost:8081/a
1797
$ curl http://localhost:8081/b
"hi"
Conclusion
You're now equipped to write webservices/web-applications using servant. The rest of this document focuses on servant-client, servant-js and servant-docs.