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/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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
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