git.iko.soy/typeable/servant
on master help sign in
add a recipe about 'structuring APIs' in general
commit6075700ebc20aed883e4f6b318f5c6f31e412ac5 authorAlp Mestanogullari <alpmestan@gmail.com> dateFri 8 Dec 2017, 22:52 (9 years ago) parent5ac6de8
3 files changed+196
cabal.project+1 doc/cookbook/index.rst+1 doc/cookbook/structuring-apis/StructuringApis.lhs+194
cabal.project+1
@@ -10,6 +10,7 @@ packages: servant/
1010 doc/cookbook/db-sqlite-simple/
1111 doc/cookbook/jwt-and-basic-auth/
1212 doc/cookbook/file-upload/
13+ doc/cookbook/structuring-apis/
1314
1415allow-newer: servant-js:servant-foreign
1516
doc/cookbook/index.rst+1
@@ -22,3 +22,4 @@ you name it!
2222 basic-auth/BasicAuth.lhs
2323 jwt-and-basic-auth/JWTAndBasicAuth.lhs
2424 file-upload/FileUpload.lhs
25+ structuring-apis/StructuringApis.lhs
doc/cookbook/structuring-apis/StructuringApis.lhsnew file+194
@@ -0,0 +1,194 @@
1+# Structuring APIs
2+
3+In this recipe, we will see a few simple ways to
4+structure your APIs by splitting them up into smaller
5+"sub-APIs" or by sharing common structure between
6+different parts. Let's start with the usual throat
7+clearing.
8+
9+``` haskell
10+{-# LANGUAGE DataKinds #-}
11+{-# LANGUAGE DeriveGeneric #-}
12+{-# LANGUAGE TypeOperators #-}
13+{-# LANGUAGE KindSignatures #-}
14+import Data.Aeson
15+import GHC.Generics
16+import GHC.TypeLits
17+import Network.Wai.Handler.Warp
18+import Servant
19+```
20+
21+Our application will consist of three different
22+"sub-APIs", with a few endpoints in each of them.
23+Our global API is defined as follows.
24+
25+``` haskell
26+type API = FactoringAPI
27+ :<|> SimpleAPI "users" User UserId
28+ :<|> SimpleAPI "products" Product ProductId
29+```
30+
31+We simply join the three different parts with `:<|>`,
32+as if each sub-API was just a simple endpoint.
33+The first part, `FactoringAPI`, shows how we can
34+"factor out" combinators that are common to several
35+endpoints, just like we turn `a * b + a * c` into
36+`a * (b + c)` in algebra.
37+
38+``` haskell
39+-- Two endpoints:
40+-- - GET /x/<some 'Int'>[?y=<some 'Int'>]
41+-- - POST /x/<some 'Int'>
42+type FactoringAPI =
43+ "x" :> Capture "x" Int :>
44+ ( QueryParam "y" Int :> Get '[JSON] Int
45+ :<|> Post '[JSON] Int
46+ )
47+
48+{- this is equivalent to:
49+
50+type FactoringAPI' =
51+ "x" :> Capture "x" Int :> QueryParam "y" Int :> Get '[JSON] Int :<|>
52+ "x" :> Capture "x" Int :> Post '[JSON] Int
53+-}
54+```
55+
56+You can see that both endpoints start with a static
57+path fragment, `/"x"`, then capture some arbitrary
58+`Int` until they finally differ. Now, this also has
59+an effect on the server for such an API, and its type
60+in particular. While the server for `FactoringAPI'` would
61+be made of a function of type `Int -> Maybe Int -> Handler Int`
62+and a function of type `Int -> Handler Int` glued with `:<|>`,
63+a server for `Factoring` (without the `'`) reflects the
64+"factorisation" and therefore, `Server FactoringAPI` is
65+`Int -> (Maybe Int -> Handler Int :<|> Handler Int)`. That is, the
66+server must be a function that takes an `Int` (the `Capture`) and
67+returns two values glued with `:<|>`, one of type `Maybe Int -> Handler Int`
68+and the other of type `Handler Int`. Let's provide such a server
69+implementation.
70+
71+_Note_: you can load this module in ghci and ask for the concrete
72+type that `Server FactoringAPI` "resolves to" by typing
73+`:kind! Server FactoringAPI`.
74+
75+``` haskell
76+factoringServer :: Server FactoringAPI
77+factoringServer x = getXY :<|> postX
78+
79+ where getXY Nothing = return x
80+ getXY (Just y) = return (x + y)
81+
82+ postX = return (x - 1)
83+```
84+
85+Next come the two sub-APIs defined in terms of this `SimpleAPI`
86+type, but with different parameters. That type is just a good old
87+Haskell type synonym that abstracts away a pretty common structure in
88+web services, where you have:
89+
90+ - one endpoint for listing a bunch of entities of some type
91+ - one endpoint for accessing the entity with a given identifier
92+ - one endpoint for creating a new entity
93+
94+There are many variants on this theme (endpoints for deleting,
95+paginated listings, etc). The simple definition below reproduces
96+such a structure, but instead of picking concrete types for
97+the entities and their identifiers, we simply let the user
98+of the type decide, by making those types parameters of
99+`SimpleAPI`. While we're at it, we'll put all our endpoints
100+under a common prefix that we also take as a parameter.
101+
102+``` haskell
103+-- Three endpoints:
104+-- - GET /<name>
105+-- - GET /<name>/<some 'i'>
106+-- - POST /<name>
107+type SimpleAPI (name :: Symbol) a i = name :>
108+ ( Get '[JSON] [a]
109+ :<|> Capture "id" i :> Get '[JSON] a
110+ :<|> ReqBody '[JSON] a :> Post '[JSON] NoContent
111+ )
112+```
113+
114+`Symbol` is the [kind](https://wiki.haskell.org/Kind)
115+of type-level strings, which is what servant uses for
116+representing static path fragments. We can even provide
117+a little helper function for creating a server for that API
118+given one handler for each endpoint as arguments.
119+
120+``` haskell
121+simpleServer
122+ :: Handler [a]
123+ -> (i -> Handler a)
124+ -> (a -> Handler NoContent)
125+ -> Server (SimpleAPI name a i)
126+simpleServer listAs getA postA =
127+ listAs :<|> getA :<|> postA
128+
129+{- you could alternatively provide such a definition
130+ but with the handlers running in another monad,
131+ or even an arbitrary one!
132+
133+simpleAPIServer
134+ :: m [a]
135+ -> (i -> m a)
136+ -> (a -> m NoContent)
137+ -> Server (SimpleAPI name a i) m
138+simpleAPIServer listAs getA postA =
139+ listAs :<|> getA :<|> postA
140+
141+ and use 'hoistServer' on the result of `simpleAPIServer`
142+ applied to your handlers right before you call `serve`.
143+-}
144+```
145+
146+We can use this to define servers for the user and product
147+related sections of the API.
148+
149+``` haskell
150+userServer :: Server (SimpleAPI "users" User UserId)
151+userServer = simpleServer
152+ (return [])
153+ (\userid -> return $
154+ if userid == 0
155+ then User "john" 64
156+ else User "everybody else" 10
157+ )
158+ (\_user -> return NoContent)
159+
160+productServer :: Server (SimpleAPI "products" Product ProductId)
161+productServer = simpleServer
162+ (return [])
163+ (\_productid -> return $ Product "Great stuff")
164+ (\_product -> return NoContent)
165+```
166+
167+Finally, some dummy types and the serving part.
168+
169+``` haskell
170+type UserId = Int
171+
172+data User = User { username :: String, age :: Int }
173+ deriving Generic
174+
175+instance FromJSON User
176+instance ToJSON User
177+
178+type ProductId = Int
179+
180+data Product = Product { productname :: String }
181+ deriving Generic
182+
183+instance FromJSON Product
184+instance ToJSON Product
185+
186+api :: Proxy API
187+api = Proxy
188+
189+main :: IO ()
190+main = run 8080 . serve api $
191+ factoringServer :<|> userServer :<|> productServer
192+```
193+
194+This program is available as a cabal project [here]().