add a recipe about 'structuring APIs' in general

This commit is contained in:
Alp Mestanogullari 2017-12-08 22:52:33 +01:00
parent 5ac6de8277
commit 6075700ebc
3 changed files with 196 additions and 0 deletions

View file

@ -10,6 +10,7 @@ packages: servant/
doc/cookbook/db-sqlite-simple/ doc/cookbook/db-sqlite-simple/
doc/cookbook/jwt-and-basic-auth/ doc/cookbook/jwt-and-basic-auth/
doc/cookbook/file-upload/ doc/cookbook/file-upload/
doc/cookbook/structuring-apis/
allow-newer: servant-js:servant-foreign allow-newer: servant-js:servant-foreign

View file

@ -22,3 +22,4 @@ you name it!
basic-auth/BasicAuth.lhs basic-auth/BasicAuth.lhs
jwt-and-basic-auth/JWTAndBasicAuth.lhs jwt-and-basic-auth/JWTAndBasicAuth.lhs
file-upload/FileUpload.lhs file-upload/FileUpload.lhs
structuring-apis/StructuringApis.lhs

View file

@ -0,0 +1,194 @@
# 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.
``` haskell
{-# 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.
``` haskell
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.
``` haskell
-- 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 `Factoring` (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.
_Note_: you can load this module in ghci and ask for the concrete
type that `Server FactoringAPI` "resolves to" by typing
`:kind! Server FactoringAPI`.
``` haskell
factoringServer :: Server FactoringAPI
factoringServer x = getXY :<|> postX
where getXY Nothing = return x
getXY (Just y) = return (x + y)
postX = return (x - 1)
```
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.
``` haskell
-- 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](https://wiki.haskell.org/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.
``` haskell
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.
``` haskell
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.
``` haskell
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]().