Compare commits
8 commits
master
...
inserting_
Author | SHA1 | Date | |
---|---|---|---|
|
1ca98aa840 | ||
|
53f722d41c | ||
|
ddfbac9dfc | ||
|
2defee6b26 | ||
|
19e432c33e | ||
|
ece559ee6e | ||
|
485b2530c1 | ||
|
94fab6b29b |
6 changed files with 431 additions and 9 deletions
|
@ -35,6 +35,7 @@ packages:
|
||||||
doc/cookbook/db-sqlite-simple
|
doc/cookbook/db-sqlite-simple
|
||||||
doc/cookbook/file-upload
|
doc/cookbook/file-upload
|
||||||
doc/cookbook/generic
|
doc/cookbook/generic
|
||||||
|
doc/cookbook/namedRoutes
|
||||||
doc/cookbook/hoist-server-with-context
|
doc/cookbook/hoist-server-with-context
|
||||||
doc/cookbook/https
|
doc/cookbook/https
|
||||||
doc/cookbook/jwt-and-basic-auth
|
doc/cookbook/jwt-and-basic-auth
|
||||||
|
|
|
@ -1,4 +1,87 @@
|
||||||
# Using generics
|
# Record-based APIs: the simple case
|
||||||
|
|
||||||
|
This cookbook explains how to implement an API with a simple record-based
|
||||||
|
structure. We only deal with non-nested APIs in which every endpoint is on the same
|
||||||
|
level.
|
||||||
|
|
||||||
|
If a you need nesting because you have different branches in your API tree, you
|
||||||
|
might want to jump directly to the [Record-based APIs: the nested records
|
||||||
|
case](../namedRoutes/NamedRoutes.html) cookbook that broaches the subject.
|
||||||
|
|
||||||
|
Shall we begin?
|
||||||
|
|
||||||
|
## Why would I want to use `Records` over the alternative `:<|>` operator?
|
||||||
|
|
||||||
|
With a record-based API, we don’t need to care about the declaration order of the endpoints.
|
||||||
|
For example, with the `:<|>` operator there’s room for error when the order of the API type
|
||||||
|
|
||||||
|
```haskell,ignore
|
||||||
|
type API1 = "version" :> Get '[JSON] Version
|
||||||
|
:<|> "movies" :> Get '[JSON] [Movie]
|
||||||
|
```
|
||||||
|
does not follow the `Handler` implementation order
|
||||||
|
```haskell,ignore
|
||||||
|
apiHandler :: ServerT API1 Handler
|
||||||
|
apiHandler = getMovies
|
||||||
|
:<|> getVersion
|
||||||
|
```
|
||||||
|
GHC could scold you with a very tedious message such as :
|
||||||
|
```console
|
||||||
|
• Couldn't match type 'Handler NoContent'
|
||||||
|
with 'Movie -> Handler NoContent'
|
||||||
|
Expected type: ServerT MovieCatalogAPI Handler
|
||||||
|
Actual type: Handler Version
|
||||||
|
:<|> ((Maybe SortBy -> Handler [Movie])
|
||||||
|
:<|> ((MovieId -> Handler (Maybe Movie))
|
||||||
|
:<|> ((MovieId -> Movie -> Handler NoContent)
|
||||||
|
:<|> (MovieId -> Handler NoContent))))
|
||||||
|
• In the expression:
|
||||||
|
versionHandler
|
||||||
|
:<|>
|
||||||
|
movieListHandler
|
||||||
|
:<|>
|
||||||
|
getMovieHandler :<|> updateMovieHandler :<|> deleteMovieHandler
|
||||||
|
In an equation for 'server':
|
||||||
|
server
|
||||||
|
= versionHandler
|
||||||
|
:<|>
|
||||||
|
movieListHandler
|
||||||
|
:<|>
|
||||||
|
getMovieHandler :<|> updateMovieHandler :<|> deleteMovieHandler
|
||||||
|
|
|
||||||
|
226 | server = versionHandler
|
||||||
|
```
|
||||||
|
On the contrary, with the record-based technique, we refer to the routes by their name:
|
||||||
|
```haskell,ignore
|
||||||
|
data API mode = API
|
||||||
|
{ list :: "list" :> ...
|
||||||
|
, delete :: "delete" :> ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
and GHC follows the lead :
|
||||||
|
```console
|
||||||
|
• Couldn't match type 'NoContent' with 'Movie'
|
||||||
|
Expected type: AsServerT Handler :- Delete '[JSON] Movie
|
||||||
|
Actual type: Handler NoContent
|
||||||
|
• In the 'delete' field of a record
|
||||||
|
In the expression:
|
||||||
|
MovieAPI
|
||||||
|
{get = getMovieHandler movieId,
|
||||||
|
update = updateMovieHandler movieId,
|
||||||
|
delete = deleteMovieHandler movieId}
|
||||||
|
In an equation for 'movieHandler':
|
||||||
|
movieHandler movieId
|
||||||
|
= MovieAPI
|
||||||
|
{get = getMovieHandler movieId,
|
||||||
|
update = updateMovieHandler movieId,
|
||||||
|
delete = deleteMovieHandler movieId}
|
||||||
|
|
|
||||||
|
252 | , delete = deleteMovieHandler movieId
|
||||||
|
```
|
||||||
|
|
||||||
|
So, records are more readable for a human, and GHC gives you more accurate error messages.
|
||||||
|
|
||||||
|
What are we waiting for?
|
||||||
|
|
||||||
```haskell
|
```haskell
|
||||||
{-# LANGUAGE DataKinds #-}
|
{-# LANGUAGE DataKinds #-}
|
||||||
|
@ -22,12 +105,12 @@ import Servant.Server.Generic
|
||||||
```
|
```
|
||||||
|
|
||||||
The usage is simple, if you only need a collection of routes.
|
The usage is simple, if you only need a collection of routes.
|
||||||
First you define a record with field types prefixed by a parameter `route`:
|
First you define a record with field types prefixed by a parameter `mode`:
|
||||||
|
|
||||||
```haskell
|
```haskell
|
||||||
data Routes route = Routes
|
data Routes mode = Routes
|
||||||
{ _get :: route :- Capture "id" Int :> Get '[JSON] String
|
{ _get :: mode :- Capture "id" Int :> Get '[JSON] String
|
||||||
, _put :: route :- ReqBody '[JSON] Int :> Put '[JSON] Bool
|
, _put :: mode :- ReqBody '[JSON] Int :> Put '[JSON] Bool
|
||||||
}
|
}
|
||||||
deriving (Generic)
|
deriving (Generic)
|
||||||
```
|
```
|
||||||
|
@ -110,7 +193,7 @@ main = do
|
||||||
_ -> putStrLn "To run, pass 'run' argument: cabal new-run cookbook-generic run"
|
_ -> putStrLn "To run, pass 'run' argument: cabal new-run cookbook-generic run"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Using generics together with a custom monad
|
## Using record-based APIs together with a custom monad
|
||||||
|
|
||||||
If your app uses a custom monad, here's how you can combine it with
|
If your app uses a custom monad, here's how you can combine it with
|
||||||
generics.
|
generics.
|
||||||
|
@ -121,9 +204,6 @@ data AppCustomState =
|
||||||
|
|
||||||
type AppM = ReaderT AppCustomState Handler
|
type AppM = ReaderT AppCustomState Handler
|
||||||
|
|
||||||
apiMyMonad :: Proxy (ToServantApi Routes)
|
|
||||||
apiMyMonad = genericApi (Proxy :: Proxy Routes)
|
|
||||||
|
|
||||||
getRouteMyMonad :: Int -> AppM String
|
getRouteMyMonad :: Int -> AppM String
|
||||||
getRouteMyMonad = return . show
|
getRouteMyMonad = return . show
|
||||||
|
|
||||||
|
@ -139,3 +219,4 @@ nt s x = runReaderT x s
|
||||||
|
|
||||||
appMyMonad :: AppCustomState -> Application
|
appMyMonad :: AppCustomState -> Application
|
||||||
appMyMonad state = genericServeT (nt state) recordMyMonad
|
appMyMonad state = genericServeT (nt state) recordMyMonad
|
||||||
|
```
|
||||||
|
|
|
@ -19,6 +19,7 @@ you name it!
|
||||||
|
|
||||||
structuring-apis/StructuringApis.lhs
|
structuring-apis/StructuringApis.lhs
|
||||||
generic/Generic.lhs
|
generic/Generic.lhs
|
||||||
|
namedRoutes/NamedRoutes.lhs
|
||||||
https/Https.lhs
|
https/Https.lhs
|
||||||
db-mysql-basics/MysqlBasics.lhs
|
db-mysql-basics/MysqlBasics.lhs
|
||||||
db-sqlite-simple/DBConnection.lhs
|
db-sqlite-simple/DBConnection.lhs
|
||||||
|
|
296
doc/cookbook/namedRoutes/NamedRoutes.lhs
Normal file
296
doc/cookbook/namedRoutes/NamedRoutes.lhs
Normal file
|
@ -0,0 +1,296 @@
|
||||||
|
# Record-based APIs: the nested records case
|
||||||
|
|
||||||
|
*Available in Servant 0.19 or higher*
|
||||||
|
|
||||||
|
Servant offers a very natural way of constructing APIs with nested records, called `NamedRoutes`.
|
||||||
|
|
||||||
|
This cookbook explains how to implement such nested-record-based-APIs using
|
||||||
|
`NamedRoutes` through the example of a Movie Catalog.
|
||||||
|
If you don't need the nested aspect of the record-based API, you might want to look at [Record-based
|
||||||
|
APIs: the simple
|
||||||
|
case](../generic/Generic.html) cookbook
|
||||||
|
which covers a simpler implementation in which every endpoint is on the same
|
||||||
|
level.
|
||||||
|
|
||||||
|
First, we start by constructing the domain types of our Movie Catalog.
|
||||||
|
After, we show you how to implement the API type with the NamedRoutes records.
|
||||||
|
Lastly, we make a Server and a Client out of the API type.
|
||||||
|
|
||||||
|
However, it should be understood that this cookbook does _not_ dwell on the
|
||||||
|
built-in servant combinators as the [Structuring APIs
|
||||||
|
](<../structuring-apis/StructuringApis.html>) cookbook already covers that angle.
|
||||||
|
|
||||||
|
|
||||||
|
## Boilerplate time!
|
||||||
|
|
||||||
|
First, let’s get rid of the the extensions and imports boilerplate in order to focus on our new technique:
|
||||||
|
|
||||||
|
|
||||||
|
```haskell
|
||||||
|
{-# LANGUAGE DataKinds #-}
|
||||||
|
{-# LANGUAGE DeriveAnyClass #-}
|
||||||
|
{-# LANGUAGE DerivingStrategies #-}
|
||||||
|
{-# LANGUAGE DeriveGeneric #-}
|
||||||
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
{-# LANGUAGE TypeOperators #-}
|
||||||
|
|
||||||
|
import GHC.Generics ( Generic )
|
||||||
|
import Data.Aeson ( FromJSON, ToJSON )
|
||||||
|
import Data.Proxy ( Proxy(..) )
|
||||||
|
import Network.Wai.Handler.Warp ( run )
|
||||||
|
|
||||||
|
import Servant ( NamedRoutes
|
||||||
|
, Handler, serve )
|
||||||
|
import Servant.API (Capture, Delete, Get, Put, QueryParam, ReqBody
|
||||||
|
, JSON, NoContent (..)
|
||||||
|
, FromHttpApiData (..),ToHttpApiData(..)
|
||||||
|
, (:>) )
|
||||||
|
import Servant.API.Generic ( (:-) )
|
||||||
|
|
||||||
|
import Servant.Client ( AsClientT, ClientM, client
|
||||||
|
, (//), (/:) )
|
||||||
|
import Servant.Client.Generic ()
|
||||||
|
|
||||||
|
import Servant.Server ( Application, ServerT )
|
||||||
|
import Servant.Server.Generic ( AsServer )
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## Domain context
|
||||||
|
|
||||||
|
Now that we’ve handled the boilerplate, we can dive into our Movie Catalog domain.
|
||||||
|
|
||||||
|
Consider a `Movie` constructed from a `Title` and a `Year` of publication.
|
||||||
|
|
||||||
|
``` haskell
|
||||||
|
data Movie = Movie
|
||||||
|
{ movieId :: MovieId
|
||||||
|
, title :: Title
|
||||||
|
, year :: Year
|
||||||
|
}
|
||||||
|
deriving stock Generic
|
||||||
|
deriving anyclass (FromJSON, ToJSON)
|
||||||
|
|
||||||
|
type MovieId = String
|
||||||
|
type Title = String
|
||||||
|
type Year = Int
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Let’s forget about the deriving stuff for now and think about the API that we want to make.
|
||||||
|
|
||||||
|
```
|
||||||
|
"version" -> Get Version
|
||||||
|
/
|
||||||
|
api "list" -> Get [Movie] ?sortBy= Title | Year (sort by the Title or the Year)
|
||||||
|
\ /
|
||||||
|
"movies" Get Movie
|
||||||
|
\ /
|
||||||
|
Capture MovieId - Put Movie
|
||||||
|
\
|
||||||
|
Delete MovieId
|
||||||
|
```
|
||||||
|
|
||||||
|
In this example, we create a very simple endpoint for the Version,
|
||||||
|
and several complex endpoints that use nested records for the CRUD part of the movie.
|
||||||
|
|
||||||
|
So, the URLs would look like
|
||||||
|
|
||||||
|
- GET …/version
|
||||||
|
- GET …/movies/list?sortby=Title
|
||||||
|
- GET …/movies/<MovieId>/
|
||||||
|
- PUT …/movies/<MovieId>/
|
||||||
|
- DELETE …/movies/<MovieId>
|
||||||
|
|
||||||
|
### API Type
|
||||||
|
|
||||||
|
Now that we have a very clear idea of the API we want to make, we need to transform it into usable Haskell code:
|
||||||
|
|
||||||
|
``` haskell
|
||||||
|
|
||||||
|
data API mode = API
|
||||||
|
{ version :: mode :- "version" :> Get '[JSON] Version
|
||||||
|
, movies :: mode :- "movies" :> NamedRoutes MoviesAPI
|
||||||
|
} deriving stock Generic
|
||||||
|
|
||||||
|
type Version = String -- This will do for the sake of example.
|
||||||
|
|
||||||
|
```
|
||||||
|
Here, we see the first node of our tree. It contains the two branches “version” and “movies” respectively:
|
||||||
|
|
||||||
|
The “version” branch is very simple and self-explanatory.
|
||||||
|
The “movies” branch will contain another node, represented by another record (see above). That is why we need the `NameRoutes` helper.
|
||||||
|
|
||||||
|
Note:
|
||||||
|
|
||||||
|
The `mode` type parameter indicates into which implementation the record’s `Generic` representation will be transformed—as a client or as a server. We will discuss that later.
|
||||||
|
|
||||||
|
Let's jump into the "movies" subtree node:
|
||||||
|
|
||||||
|
|
||||||
|
``` haskell
|
||||||
|
|
||||||
|
data MoviesAPI mode = MoviesAPI
|
||||||
|
{ list :: mode :- "list" :> QueryParam "SortBy" SortBy :> Get '[JSON] [Movie]
|
||||||
|
, movie :: mode :- Capture "movieId" MovieId :> NamedRoutes MovieAPI
|
||||||
|
} deriving stock Generic
|
||||||
|
|
||||||
|
data SortBy = Year | Title
|
||||||
|
|
||||||
|
instance ToHttpApiData SortBy where
|
||||||
|
toQueryParam Year = "year"
|
||||||
|
toQueryParam Title = "title"
|
||||||
|
|
||||||
|
instance FromHttpApiData SortBy where
|
||||||
|
parseQueryParam "year" = Right Year
|
||||||
|
parseQueryParam "title" = Right Title
|
||||||
|
parseQueryParam param = Left $ param <> " is not a valid value"
|
||||||
|
|
||||||
|
```
|
||||||
|
So, remember, this type represents the `MoviesAPI` node that we’ve connected earlier to the main `API` tree.
|
||||||
|
|
||||||
|
In this subtree, we illustrated both an endpoint with a **query param** and also, a **capture** with a subtree underneath it.
|
||||||
|
|
||||||
|
So, let's go deeper into our API tree.
|
||||||
|
|
||||||
|
``` haskell
|
||||||
|
data MovieAPI mode = MovieAPI
|
||||||
|
{ get :: mode :- Get '[JSON] (Maybe Movie)
|
||||||
|
, update :: mode :- ReqBody '[JSON] Movie :> Put '[JSON] NoContent
|
||||||
|
, delete :: mode :- Delete '[JSON] NoContent
|
||||||
|
} deriving stock Generic
|
||||||
|
```
|
||||||
|
|
||||||
|
As you can see, we end up implementing the deepest routes of our API.
|
||||||
|
|
||||||
|
Small detail: as our main API tree is also a record, we need the `NamedRoutes` helper.
|
||||||
|
To improve readability, we suggest you create a type alias:
|
||||||
|
|
||||||
|
``` haskell
|
||||||
|
type MovieCatalogAPI = NamedRoutes API
|
||||||
|
```
|
||||||
|
|
||||||
|
That's it, we have our `MovieCatalogAPI` type!
|
||||||
|
|
||||||
|
Let's make a server and a client out of it!
|
||||||
|
|
||||||
|
## The Server
|
||||||
|
|
||||||
|
As you know, we can’t talk about a server, without addressing the handlers.
|
||||||
|
|
||||||
|
First, we take our handlers…
|
||||||
|
|
||||||
|
```haskell
|
||||||
|
versionHandler :: Handler Version
|
||||||
|
versionHandler = pure "0.0.1"
|
||||||
|
|
||||||
|
movieListHandler :: Maybe SortBy -> Handler [Movie]
|
||||||
|
movieListHandler _ = pure moviesDB
|
||||||
|
|
||||||
|
moviesDB :: [Movie]
|
||||||
|
moviesDB =
|
||||||
|
[ Movie "1" "Se7en" 1995
|
||||||
|
, Movie "2" "Minority Report" 2002
|
||||||
|
, Movie "3" "The Godfather" 1972
|
||||||
|
]
|
||||||
|
|
||||||
|
getMovieHandler :: MovieId -> Handler (Maybe Movie)
|
||||||
|
getMovieHandler requestMovieId = go moviesDB
|
||||||
|
where
|
||||||
|
go [] = pure Nothing
|
||||||
|
go (movie:ms) | movieId movie == requestMovieId = pure $ Just movie
|
||||||
|
go (m:ms) = go ms
|
||||||
|
|
||||||
|
updateMovieHandler :: MovieId -> Movie -> Handler NoContent
|
||||||
|
updateMovieHandler requestedMovieId newMovie =
|
||||||
|
-- update the movie list in the database...
|
||||||
|
pure NoContent
|
||||||
|
|
||||||
|
deleteMovieHandler :: MovieId -> Handler NoContent
|
||||||
|
deleteMovieHandler _ =
|
||||||
|
-- delete the movie from the database...
|
||||||
|
pure NoContent
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
And assemble them together with the record structure, which is the glue here.
|
||||||
|
|
||||||
|
```haskell
|
||||||
|
server :: API AsServer
|
||||||
|
server =
|
||||||
|
API
|
||||||
|
{ version = versionHandler
|
||||||
|
, movies = moviesHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
moviesHandler :: MoviesAPI AsServer
|
||||||
|
moviesHandler =
|
||||||
|
MoviesAPI
|
||||||
|
{ list = movieListHandler
|
||||||
|
, movie = movieHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
movieHandler :: MovieId -> MovieAPI AsServer
|
||||||
|
movieHandler movieId = MovieAPI
|
||||||
|
{ get = getMovieHandler movieId
|
||||||
|
, update = updateMovieHandler movieId
|
||||||
|
, delete = deleteMovieHandler movieId
|
||||||
|
}
|
||||||
|
```
|
||||||
|
As you might have noticed, we build our handlers out of the same record types we used to define our API: `MoviesAPI` and `MovieAPI`. What kind of magic is this ?
|
||||||
|
|
||||||
|
Finally, we can run the server and connect the API routes to the handlers as usual:
|
||||||
|
|
||||||
|
``` haskell
|
||||||
|
api :: Proxy MovieCatalogAPI
|
||||||
|
api = Proxy
|
||||||
|
|
||||||
|
main :: IO ()
|
||||||
|
main = run 8081 app
|
||||||
|
|
||||||
|
app :: Application
|
||||||
|
app = serve api server
|
||||||
|
|
||||||
|
```
|
||||||
|
Yay! That’s done and we’ve got our server!
|
||||||
|
|
||||||
|
## The Client
|
||||||
|
|
||||||
|
The client, so to speak, is very easy to implement:
|
||||||
|
|
||||||
|
``` haskell
|
||||||
|
movieCatalogClient :: API (AsClientT ClientM)
|
||||||
|
movieCatalogClient = client api -- remember: api :: Proxy MovieCatalogAPI
|
||||||
|
```
|
||||||
|
|
||||||
|
We’ve also introduced some operators that help navigate through the nested records.
|
||||||
|
|
||||||
|
`(//)` is used to jump from one record to another.
|
||||||
|
`(/:)` is used to provide a parameter, whether it be a query param or a capture.
|
||||||
|
|
||||||
|
Let’s use those nice helpers for our movie catalog:
|
||||||
|
|
||||||
|
```haskell
|
||||||
|
listMovies :: Maybe SortBy -> ClientM [Movie]
|
||||||
|
listMovies sortBy = movieCatalogClient // movies // list /: sortBy
|
||||||
|
|
||||||
|
getMovie :: MovieId -> ClientM (Maybe Movie)
|
||||||
|
getMovie movieId = movieCatalogClient // movies // movie /: movieId // get
|
||||||
|
|
||||||
|
updateMovie :: MovieId -> Movie -> ClientM NoContent
|
||||||
|
updateMovie movieId newMovie = movieCatalogClient // movies // movie /: movieId // update /: newMovie
|
||||||
|
|
||||||
|
deleteMovie :: MovieId -> ClientM NoContent
|
||||||
|
deleteMovie movieId = movieCatalogClient // movies // movie /: movieId // delete
|
||||||
|
```
|
||||||
|
|
||||||
|
Done! We’ve got our client!
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
We hope that you found this cookbook helpful, and that you now feel more confident using the record-based APIs, nested or not.
|
||||||
|
|
||||||
|
If you are interested in further understanding the built-in Servant combinators, see [Structuring APIs](../structuring-apis/StructuringApis.html).
|
||||||
|
|
||||||
|
Since `NamedRoutes` is based on the Generic mechanism, you might want to have a look at [Sandy Maguire’s _Thinking with Types_ book](https://doku.pub/download/sandy-maguire-thinking-with-typesz-liborgpdf-4lo5ne7kdj0x).
|
27
doc/cookbook/namedRoutes/namedRoutes.cabal
Normal file
27
doc/cookbook/namedRoutes/namedRoutes.cabal
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
name: namedRoutes
|
||||||
|
version: 0.1
|
||||||
|
synopsis: NamedRoutes - Generic servant API implementation cookbook example
|
||||||
|
homepage: http://docs.servant.dev/
|
||||||
|
license: BSD3
|
||||||
|
license-file: ../../../servant/LICENSE
|
||||||
|
author: Servant Contributors
|
||||||
|
maintainer: haskell-servant-maintainers@googlegroups.com
|
||||||
|
build-type: Simple
|
||||||
|
cabal-version: >=1.10
|
||||||
|
tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.1
|
||||||
|
|
||||||
|
executable namedRoutes
|
||||||
|
main-is: NamedRoutes.lhs
|
||||||
|
build-depends: base == 4.*
|
||||||
|
, aeson >= 1.2
|
||||||
|
, text
|
||||||
|
, servant
|
||||||
|
, servant-client
|
||||||
|
, servant-client-core
|
||||||
|
, servant-server
|
||||||
|
, wai >= 3.2
|
||||||
|
, warp >= 3.2
|
||||||
|
|
||||||
|
default-language: Haskell2010
|
||||||
|
ghc-options: -Wall -pgmL markdown-unlit
|
||||||
|
build-tool-depends: markdown-unlit:markdown-unlit
|
|
@ -35,6 +35,11 @@ The first part, `FactoringAPI`, shows how we can
|
||||||
endpoints, just like we turn `a * b + a * c` into
|
endpoints, just like we turn `a * b + a * c` into
|
||||||
`a * (b + c)` in algebra.
|
`a * (b + c)` in algebra.
|
||||||
|
|
||||||
|
(It should be noted that the `(:<|>)` operator is not the only way of combining
|
||||||
|
endpoints with Servant. Other techniques are shown in subsequent cookbooks. See
|
||||||
|
[record-based alternative for implementing APIs](StructuringApis.html#record-based-alternative-for-implementing-apis))
|
||||||
|
|
||||||
|
|
||||||
``` haskell
|
``` haskell
|
||||||
-- Two endpoints:
|
-- Two endpoints:
|
||||||
-- - GET /x/<some 'Int'>[?y=<some 'Int'>]
|
-- - GET /x/<some 'Int'>[?y=<some 'Int'>]
|
||||||
|
@ -203,3 +208,14 @@ main = run 8080 . serve api $
|
||||||
|
|
||||||
This program is available as a cabal project
|
This program is available as a cabal project
|
||||||
[here](https://github.com/haskell-servant/servant/tree/master/doc/cookbook/structuring-apis).
|
[here](https://github.com/haskell-servant/servant/tree/master/doc/cookbook/structuring-apis).
|
||||||
|
|
||||||
|
## Record-based alternative for implementing APIs
|
||||||
|
|
||||||
|
It should be noted that the `(:<|>)` is not the only way of combining endpoints.
|
||||||
|
Servant offers a convenient way to design APIs with records avoiding the ordering constraint of the operator.
|
||||||
|
|
||||||
|
A simple case is approached in the [Record-based APIs: the simple
|
||||||
|
case](../generic/Generic.html)
|
||||||
|
cookbook, which deals with flat APIs where every endpoint is on the same level.
|
||||||
|
Also, a more complex example with nested record is discussed in [Record-based APIs: the nested
|
||||||
|
records case](../namedRoutes/NamedRoutes.html) in which we implement an API tree with many branches.
|
||||||
|
|
Loading…
Add table
Reference in a new issue