@@ -1,373 +0,0 @@
1−# NamedRoutes - Using records to define APIs
3−*Available in Servant 0.19 or higher*
5−Servant offers a very natural way of constructing APIs with nested records, called `NamedRoutes`.
7−This cookbook explains how to implement such nested-record-APIs using `NamedRoutes` through the example of a Movie Catalog.
9−First, we start by constructing the domain types of our Movie Catalog.
10−After, we show you how to implement the API type with the NamedRoutes records.
11−Lastly, we make a Server and a Client out of the API type.
13−However, it should be understood that this cookbook does _not_ dwell on the built-in servant combinators as the [<Structuring APIs> cookbook ](<https://docs.servant.dev/en/stable/cookbook/structuring-apis/StructuringApis.html>) already covers that angle.
15−## Why would I want to use `NamedRoutes` over the alternative `:<|>` operator?
17−With `NamedRoutes`, we don’t need to care about the declaration order of the endpoints.
18−For example, with the `:<|>` operator there’s room for error when the order of the API type
21−type API1 = "version" :> Get '[JSON] Version
22− :<|> "movies" :> Get '[JSON] [Movie]
25−does not follow the `Handler` implementation order
28−apiHandler :: ServerT API1 Handler
29−apiHandler = getMovies
33−GHC could scold you with a very tedious message such as :
36− • Couldn't match type 'Handler NoContent'
37− with 'Movie -> Handler NoContent'
38− Expected type: ServerT MovieCatalogAPI Handler
39− Actual type: Handler Version
40− :<|> ((Maybe SortBy -> Handler [Movie])
41− :<|> ((MovieId -> Handler (Maybe Movie))
42− :<|> ((MovieId -> Movie -> Handler NoContent)
43− :<|> (MovieId -> Handler NoContent))))
49− getMovieHandler :<|> updateMovieHandler :<|> deleteMovieHandler
50− In an equation for 'server':
56− getMovieHandler :<|> updateMovieHandler :<|> deleteMovieHandler
58−226 | server = versionHandler
61−On the contrary, with the `NamedRoutes` technique, we refer to the routes by their name:
65− { list :: "list" :> ...
66− , delete :: "delete" :> ...
70−and GHC follows the lead :
73− • Couldn't match type 'NoContent' with 'Movie'
74− Expected type: AsServerT Handler :- Delete '[JSON] Movie
75− Actual type: Handler NoContent
76− • In the 'delete' field of a record
79− {get = getMovieHandler movieId,
80− update = updateMovieHandler movieId,
81− delete = deleteMovieHandler movieId}
82− In an equation for 'movieHandler':
85− {get = getMovieHandler movieId,
86− update = updateMovieHandler movieId,
87− delete = deleteMovieHandler movieId}
89−252 | , delete = deleteMovieHandler movieId
92−So, NamedRoutes is more readable for a human, and GHC gives you more accurate error messages.
94−What are we waiting for?
99−First, let’s get rid of the the extensions and imports boilerplate in order to focus on our new technique:
103−{-# LANGUAGE DataKinds #-}
104−{-# LANGUAGE DeriveAnyClass #-}
105−{-# LANGUAGE DerivingStrategies #-}
106−{-# LANGUAGE DeriveGeneric #-}
107−{-# LANGUAGE OverloadedStrings #-}
108−{-# LANGUAGE TypeOperators #-}
110−import GHC.Generics ( Generic )
111−import Data.Aeson ( FromJSON, ToJSON )
112−import Data.Proxy ( Proxy(..) )
113−import Network.Wai.Handler.Warp ( run )
115−import Servant ( NamedRoutes
117−import Servant.API (Capture, Delete, Get, Put, QueryParam, ReqBody
118− , JSON, NoContent (..)
119− , FromHttpApiData (..),ToHttpApiData(..)
121−import Servant.API.Generic ( (:-) )
123−import Servant.Client ( AsClientT, ClientM, client
125−import Servant.Client.Generic ()
127−import Servant.Server ( Application, ServerT )
128−import Servant.Server.Generic ( AsServerT )
134−Now that we’ve handled the boilerplate, we can dive into our Movie Catalog domain.
136−Consider a `Movie` constructed from a `Title` and a `Year` of publication.
140− { movieId :: MovieId
144− deriving stock Generic
145− deriving anyclass (FromJSON, ToJSON)
147−type MovieId = String
154−Let’s forget about the deriving stuff for now and think about the API that we want to make.
157− "version" -> Get Version
159−api "list" -> Get [Movie] ?sortBy= Title | Year (sort by the Title or the Year)
163− Capture MovieId - Put Movie
168−In this example, we create a very simple endpoint for the Version,
169−and several complex endpoints that use nested records for the CRUD part of the movie.
171−So, the URLs would look like
174−- GET …/movies/list?sortby=Title
175−- GET …/movies/<MovieId>/
176−- PUT …/movies/<MovieId>/
177−- DELETE …/movies/<MovieId>
181−Now that we have a very clear idea of the API we want to make, we need to transform it into usable Haskell code:
186− { version :: mode :- "version" :> Get '[JSON] Version
187− , movies :: mode :- "movies" :> NamedRoutes MoviesAPI
188− } deriving stock Generic
190−type Version = String -- This will do for the sake of example.
193−Here, we see the first node of our tree. It contains the two branches “version” and “movies” respectively:
195−The “version” branch is very simple and self-explanatory.
196−The “movies” branch will contain another node, represented by another record (see above). That is why we need the `NameRoutes` helper.
200−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.
202−Let's jump into the "movies" subtree node:
207−data MoviesAPI mode = MoviesAPI
208− { list :: mode :- "list" :> QueryParam "SortBy" SortBy :> Get '[JSON] [Movie]
209− , movie :: mode :- Capture "movieId" MovieId :> NamedRoutes MovieAPI
210− } deriving stock Generic
212−data SortBy = Year | Title
214−instance ToHttpApiData SortBy where
215− toQueryParam Year = "year"
216− toQueryParam Title = "title"
218−instance FromHttpApiData SortBy where
219− parseQueryParam "year" = Right Year
220− parseQueryParam "title" = Right Title
221− parseQueryParam param = Left $ param <> " is not a valid value"
224−So, remember, this type represents the `MoviesAPI` node that we’ve connected earlier to the main `API` tree.
226−In this subtree, we illustrated both an endpoint with a **query param** and also, a **capture** with a subtree underneath it.
228−So, let's go deeper into our API tree.
231−data MovieAPI mode = MovieAPI
232− { get :: mode :- Get '[JSON] (Maybe Movie)
233− , update :: mode :- ReqBody '[JSON] Movie :> Put '[JSON] NoContent
234− , delete :: mode :- Delete '[JSON] NoContent
235− } deriving stock Generic
238−As you can see, we end up implementing the deepest routes of our API.
240−Small detail: as our main API tree is also a record, we need the `NamedRoutes` helper.
241−To improve readability, we suggest you create a type alias:
244−type MovieCatalogAPI = NamedRoutes API
247−That's it, we have our `MovieCatalogAPI` type!
249−Let's make a server and a client out of it!
253−As you know, we can’t talk about a server, without addressing the handlers.
255−First, we take our handlers…
258−versionHandler :: Handler Version
259−versionHandler = pure "0.0.1"
261−movieListHandler :: Maybe SortBy -> Handler [Movie]
262−movieListHandler _ = pure moviesDB
266− [ Movie "1" "Se7en" 1995
267− , Movie "2" "Minority Report" 2002
268− , Movie "3" "The Godfather" 1972
271−getMovieHandler :: MovieId -> Handler (Maybe Movie)
272−getMovieHandler requestMovieId = go moviesDB
274− go [] = pure Nothing
275− go (movie:ms) | movieId movie == requestMovieId = pure $ Just movie
278−updateMovieHandler :: MovieId -> Movie -> Handler NoContent
279−updateMovieHandler requestedMovieId newMovie =
280− -- update the movie list in the database...
283−deleteMovieHandler :: MovieId -> Handler NoContent
284−deleteMovieHandler _ =
285− -- delete the movie from the database...
290−And assemble them together with the record structure, which is the glue here.
293−server :: ServerT MovieCatalogAPI Handler
296− { version = versionHandler
297− , movies = moviesHandler
300−moviesHandler :: MoviesAPI (AsServerT Handler)
303− { list = movieListHandler
304− , movie = movieHandler
307−movieHandler :: MovieId -> MovieAPI (AsServerT Handler)
308−movieHandler movieId = MovieAPI
309− { get = getMovieHandler movieId
310− , update = updateMovieHandler movieId
311− , delete = deleteMovieHandler movieId
314−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 ?
316−Remember the `mode` type parameter we saw earlier? Since we need to transform our API type into a _server_, we need to provide a server `mode`, which is `AsServerT Handler` here.
318−Finally, we can run the server and connect the API routes to the handlers as usual:
321−api :: Proxy MovieCatalogAPI
328−app = serve api server
331−Yay! That’s done and we’ve got our server!
335−The client, so to speak, is very easy to implement:
338−movieCatalogClient :: API (AsClientT ClientM)
339−movieCatalogClient = client api -- remember: api: Proxy MovieCatalogAPI
342−Have you noticed the `mode` `AsClient ClientM`?
344−We’ve also introduced some operators that help navigate through the nested records.
346−`(//)` is used to jump from one record to another.
347−`(/:)` is used to provide a parameter, whether it be a query param or a capture.
349−Let’s use those nice helpers for our movie catalog:
352−listMovies :: Maybe SortBy -> ClientM [Movie]
353−listMovies sortBy = movieCatalogClient // movies // list /: sortBy
355−getMovie :: MovieId -> ClientM (Maybe Movie)
356−getMovie movieId = movieCatalogClient // movies // movie /: movieId // get
358−updateMovie :: MovieId -> Movie -> ClientM NoContent
359−updateMovie movieId newMovie = movieCatalogClient // movies // movie /: movieId // update /: newMovie
361−deleteMovie :: MovieId -> ClientM NoContent
362−deleteMovie movieId = movieCatalogClient // movies // movie /: movieId // delete
365−Done! We’ve got our client!
369−We hope that you found this workbook helpful, and that you now feel more confident using the `NamedRoutes` technique.
371−If you are interested in further understanding the built-in Servant combinators, see [Structuring APIs](https://docs.servant.dev/en/stable/cookbook/structuring-apis/StructuringApis.html).
373−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).