From 6075700ebc20aed883e4f6b318f5c6f31e412ac5 Mon Sep 17 00:00:00 2001 From: Alp Mestanogullari Date: Fri, 8 Dec 2017 22:52:33 +0100 Subject: [PATCH] add a recipe about 'structuring APIs' in general --- cabal.project | 1 + doc/cookbook/index.rst | 1 + .../structuring-apis/StructuringApis.lhs | 194 ++++++++++++++++++ 3 files changed, 196 insertions(+) create mode 100644 doc/cookbook/structuring-apis/StructuringApis.lhs diff --git a/cabal.project b/cabal.project index 5893d321..c5859865 100644 --- a/cabal.project +++ b/cabal.project @@ -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 diff --git a/doc/cookbook/index.rst b/doc/cookbook/index.rst index d4df3798..e9e469ad 100644 --- a/doc/cookbook/index.rst +++ b/doc/cookbook/index.rst @@ -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 diff --git a/doc/cookbook/structuring-apis/StructuringApis.lhs b/doc/cookbook/structuring-apis/StructuringApis.lhs new file mode 100644 index 00000000..0a260bb3 --- /dev/null +++ b/doc/cookbook/structuring-apis/StructuringApis.lhs @@ -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/[?y=] +-- - POST /x/ +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 / +-- - GET // +-- - POST / +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]().