Structuring APIs¶
In this recipe, we will see a few simple ways to structure your APIs by splitting them up into smaller “sub-APIs” or by sharing common structure between different parts. Let’s start with the usual throat clearing.
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE KindSignatures #-}
import Data.Aeson
import GHC.Generics
import GHC.TypeLits
import Network.Wai.Handler.Warp
import Servant
Our application will consist of three different “sub-APIs”, with a few endpoints in each of them. Our global API is defined as follows.
type API = FactoringAPI
:<|> SimpleAPI "users" User UserId
:<|> SimpleAPI "products" Product ProductId
We simply join the three different parts with :<|>
,
as if each sub-API was just a simple endpoint.
The first part, FactoringAPI
, shows how we can
“factor out” combinators that are common to several
endpoints, just like we turn a * b + a * c
into
a * (b + c)
in algebra.
-- Two endpoints:
-- - GET /x/<some 'Int'>[?y=<some 'Int'>]
-- - POST /x/<some 'Int'>
type FactoringAPI =
"x" :> Capture "x" Int :>
( QueryParam "y" Int :> Get '[JSON] Int
:<|> Post '[JSON] Int
)
{- this is equivalent to:
type FactoringAPI' =
"x" :> Capture "x" Int :> QueryParam "y" Int :> Get '[JSON] Int :<|>
"x" :> Capture "x" Int :> Post '[JSON] Int
-}
You can see that both endpoints start with a static
path fragment, /"x"
, then capture some arbitrary
Int
until they finally differ. Now, this also has
an effect on the server for such an API, and its type
in particular. While the server for FactoringAPI'
would
be made of a function of type Int -> Maybe Int -> Handler Int
and a function of type Int -> Handler Int
glued with :<|>
,
a server for FactoringAPI
(without the '
) reflects the
“factorisation” and therefore, Server FactoringAPI
is
Int -> (Maybe Int -> Handler Int :<|> Handler Int)
. That is, the
server must be a function that takes an Int
(the Capture
) and
returns two values glued with :<|>
, one of type Maybe Int -> Handler Int
and the other of type Handler Int
. Let’s provide such a server
implementation, with those “nested types”.
Tip: you can load this module in ghci and ask for the concrete
type that Server FactoringAPI
“resolves to” by typing
:kind! Server FactoringAPI
.
factoringServer :: Server FactoringAPI
factoringServer x = getXY :<|> postX
where getXY Nothing = return x
getXY (Just y) = return (x + y)
postX = return (x - 1)
If you want to avoid the “nested types” and the need to manually
dispatch the arguments (like x
above) to the different request
handlers, and would just like to be able to declare the API type
as above but pretending that the Capture
is not factored out,
that every combinator is “distributed” (i.e that all endpoints
are specified like FactoringAPI'
above), then you should
look at flatten
from the
servant-flatten
package.
Next come the two sub-APIs defined in terms of this SimpleAPI
type, but with different parameters. That type is just a good old
Haskell type synonym that abstracts away a pretty common structure in
web services, where you have:
- one endpoint for listing a bunch of entities of some type
- one endpoint for accessing the entity with a given identifier
- one endpoint for creating a new entity
There are many variants on this theme (endpoints for deleting,
paginated listings, etc). The simple definition below reproduces
such a structure, but instead of picking concrete types for
the entities and their identifiers, we simply let the user
of the type decide, by making those types parameters of
SimpleAPI
. While we’re at it, we’ll put all our endpoints
under a common prefix that we also take as a parameter.
-- Three endpoints:
-- - GET /<name>
-- - GET /<name>/<some 'i'>
-- - POST /<name>
type SimpleAPI (name :: Symbol) a i = name :>
( Get '[JSON] [a]
:<|> Capture "id" i :> Get '[JSON] a
:<|> ReqBody '[JSON] a :> Post '[JSON] NoContent
)
Symbol
is the kind
of type-level strings, which is what servant uses for
representing static path fragments. We can even provide
a little helper function for creating a server for that API
given one handler for each endpoint as arguments.
simpleServer
:: Handler [a]
-> (i -> Handler a)
-> (a -> Handler NoContent)
-> Server (SimpleAPI name a i)
simpleServer listAs getA postA =
listAs :<|> getA :<|> postA
{- you could alternatively provide such a definition
but with the handlers running in another monad,
or even an arbitrary one!
simpleAPIServer
:: m [a]
-> (i -> m a)
-> (a -> m NoContent)
-> Server (SimpleAPI name a i) m
simpleAPIServer listAs getA postA =
listAs :<|> getA :<|> postA
and use 'hoistServer' on the result of `simpleAPIServer`
applied to your handlers right before you call `serve`.
-}
We can use this to define servers for the user and product related sections of the API.
userServer :: Server (SimpleAPI "users" User UserId)
userServer = simpleServer
(return [])
(\userid -> return $
if userid == 0
then User "john" 64
else User "everybody else" 10
)
(\_user -> return NoContent)
productServer :: Server (SimpleAPI "products" Product ProductId)
productServer = simpleServer
(return [])
(\_productid -> return $ Product "Great stuff")
(\_product -> return NoContent)
Finally, some dummy types and the serving part.
type UserId = Int
data User = User { username :: String, age :: Int }
deriving Generic
instance FromJSON User
instance ToJSON User
type ProductId = Int
data Product = Product { productname :: String }
deriving Generic
instance FromJSON Product
instance ToJSON Product
api :: Proxy API
api = Proxy
main :: IO ()
main = run 8080 . serve api $
factoringServer :<|> userServer :<|> productServer
This program is available as a cabal project here.