add a recipe about 'structuring APIs' in general
This commit is contained in:
parent
5ac6de8277
commit
6075700ebc
3 changed files with 196 additions and 0 deletions
|
@ -10,6 +10,7 @@ packages: servant/
|
|||
doc/cookbook/db-sqlite-simple/
|
||||
doc/cookbook/jwt-and-basic-auth/
|
||||
doc/cookbook/file-upload/
|
||||
doc/cookbook/structuring-apis/
|
||||
|
||||
allow-newer: servant-js:servant-foreign
|
||||
|
||||
|
|
|
@ -22,3 +22,4 @@ you name it!
|
|||
basic-auth/BasicAuth.lhs
|
||||
jwt-and-basic-auth/JWTAndBasicAuth.lhs
|
||||
file-upload/FileUpload.lhs
|
||||
structuring-apis/StructuringApis.lhs
|
||||
|
|
194
doc/cookbook/structuring-apis/StructuringApis.lhs
Normal file
194
doc/cookbook/structuring-apis/StructuringApis.lhs
Normal 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]().
|
Loading…
Reference in a new issue