@@ -0,0 +1,373 @@
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).