195 lines
5.6 KiB
Text
195 lines
5.6 KiB
Text
# 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 `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.
|
|
|
|
_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](https://github.com/haskell-servant/servant/tree/master/doc/cookbook/structuring-apis).
|