diff --git a/.gitignore b/.gitignore
index 3b2084ae6..0a0ae1d1d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -35,6 +35,8 @@ shell.nix
# nix
result*
+.direnv
+.pre-commit-config.yaml
# local versions of things
servant-multipart
diff --git a/cabal.project b/cabal.project
index 5c8c2a70f..206f6b5d3 100644
--- a/cabal.project
+++ b/cabal.project
@@ -36,7 +36,7 @@ packages:
doc/cookbook/db-postgres-pool
doc/cookbook/db-sqlite-simple
doc/cookbook/file-upload
- doc/cookbook/generic
+ doc/cookbook/named-routes
doc/cookbook/hoist-server-with-context
doc/cookbook/https
doc/cookbook/jwt-and-basic-auth
diff --git a/doc/cookbook/generic/Generic.lhs b/doc/cookbook/generic/Generic.lhs
deleted file mode 100644
index 45180230a..000000000
--- a/doc/cookbook/generic/Generic.lhs
+++ /dev/null
@@ -1,141 +0,0 @@
-# Using generics
-
-```haskell
-{-# LANGUAGE DataKinds #-}
-{-# LANGUAGE DeriveGeneric #-}
-{-# LANGUAGE RankNTypes #-}
-{-# LANGUAGE TypeOperators #-}
-module Main (main, api, getLink, routesLinks, cliGet) where
-
-import Control.Exception (throwIO)
-import Control.Monad.Trans.Reader (ReaderT, runReaderT)
-import Data.Proxy (Proxy (..))
-import Network.Wai.Handler.Warp (run)
-import System.Environment (getArgs)
-
-import Servant
-import Servant.Client
-
-import Servant.API.Generic
-import Servant.Client.Generic
-import Servant.Server.Generic
-```
-
-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`:
-
-```haskell
-data Routes route = Routes
- { _get :: route :- Capture "id" Int :> Get '[JSON] String
- , _put :: route :- ReqBody '[JSON] Int :> Put '[JSON] Bool
- }
- deriving (Generic)
-```
-
-Then we'll use this data type to define API, links, server and client.
-
-## API
-
-You can get a `Proxy` of the API using `genericApi`:
-
-```haskell
-api :: Proxy (ToServantApi Routes)
-api = genericApi (Proxy :: Proxy Routes)
-```
-
-It's recommended to use `genericApi` function, as then you'll get
-better error message, for example if you forget to `derive Generic`.
-
-## Links
-
-The clear advantage of record-based generics approach, is that
-we can get safe links very conveniently. We don't need to define endpoint types,
-as field accessors work as proxies:
-
-```haskell
-getLink :: Int -> Link
-getLink = fieldLink _get
-```
-
-We can also get all links at once, as a record:
-
-```haskell
-routesLinks :: Routes (AsLink Link)
-routesLinks = allFieldLinks
-```
-
-## Client
-
-Even more power starts to show when we generate a record of client functions.
-Here we use `genericClientHoist` function, which lets us simultaneously
-hoist the monad, in this case from `ClientM` to `IO`.
-
-```haskell
-cliRoutes :: Routes (AsClientT IO)
-cliRoutes = genericClientHoist
- (\x -> runClientM x env >>= either throwIO return)
- where
- env = error "undefined environment"
-
-cliGet :: Int -> IO String
-cliGet = _get cliRoutes
-```
-
-## Server
-
-Finally, probably the most handy usage: we can convert record of handlers into
-the server implementation:
-
-```haskell
-record :: Routes AsServer
-record = Routes
- { _get = return . show
- , _put = return . odd
- }
-
-app :: Application
-app = genericServe record
-
-main :: IO ()
-main = do
- args <- getArgs
- case args of
- ("run":_) -> do
- putStrLn "Starting cookbook-generic at http://localhost:8000"
- run 8000 app
- -- see this cookbook below for custom-monad explanation
- ("run-custom-monad":_) -> do
- putStrLn "Starting cookbook-generic with a custom monad at http://localhost:8000"
- run 8000 (appMyMonad AppCustomState)
- _ -> putStrLn "To run, pass 'run' argument: cabal new-run cookbook-generic run"
-```
-
-## Using generics together with a custom monad
-
-If your app uses a custom monad, here's how you can combine it with
-generics.
-
-```haskell
-data AppCustomState =
- AppCustomState
-
-type AppM = ReaderT AppCustomState Handler
-
-apiMyMonad :: Proxy (ToServantApi Routes)
-apiMyMonad = genericApi (Proxy :: Proxy Routes)
-
-getRouteMyMonad :: Int -> AppM String
-getRouteMyMonad = return . show
-
-putRouteMyMonad :: Int -> AppM Bool
-putRouteMyMonad = return . odd
-
-recordMyMonad :: Routes (AsServerT AppM)
-recordMyMonad = Routes {_get = getRouteMyMonad, _put = putRouteMyMonad}
-
--- natural transformation
-nt :: AppCustomState -> AppM a -> Handler a
-nt s x = runReaderT x s
-
-appMyMonad :: AppCustomState -> Application
-appMyMonad state = genericServeT (nt state) recordMyMonad
diff --git a/doc/cookbook/generic/generic.cabal b/doc/cookbook/generic/generic.cabal
deleted file mode 100644
index 725f70c97..000000000
--- a/doc/cookbook/generic/generic.cabal
+++ /dev/null
@@ -1,25 +0,0 @@
-cabal-version: 2.2
-name: cookbook-generic
-version: 0.1
-synopsis: Using custom monad to pass a state between handlers
-homepage: http://docs.servant.dev/
-license: BSD-3-Clause
-license-file: ../../../servant/LICENSE
-author: Servant Contributors
-maintainer: haskell-servant-maintainers@googlegroups.com
-build-type: Simple
-tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
-
-executable cookbook-using-custom-monad
- main-is: Generic.lhs
- build-depends: base == 4.*
- , servant
- , servant-client
- , servant-client-core
- , servant-server
- , base-compat
- , warp >= 3.2
- , transformers >= 0.3
- default-language: Haskell2010
- ghc-options: -Wall -pgmL markdown-unlit
- build-tool-depends: markdown-unlit:markdown-unlit >= 0.4
diff --git a/doc/cookbook/index.rst b/doc/cookbook/index.rst
index 79a0179bf..6a8b12041 100644
--- a/doc/cookbook/index.rst
+++ b/doc/cookbook/index.rst
@@ -20,6 +20,7 @@ you name it!
structuring-apis/StructuringApis.lhs
generic/Generic.lhs
openapi3/OpenAPI.lhs
+ named-routes/NamedRoutes.lhs
https/Https.lhs
db-mysql-basics/MysqlBasics.lhs
db-sqlite-simple/DBConnection.lhs
diff --git a/doc/cookbook/named-routes/NamedRoutes.lhs b/doc/cookbook/named-routes/NamedRoutes.lhs
new file mode 100644
index 000000000..0226fefa5
--- /dev/null
+++ b/doc/cookbook/named-routes/NamedRoutes.lhs
@@ -0,0 +1,393 @@
+# Record-based APIs
+
+*Available in Servant 0.19 or higher*
+
+Servant offers a very natural way of constructing APIs with records and nested records.
+
+This cookbook explains how to implement APIs using records.
+
+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.
+
+## Motivation: Why would I want to use records over the `:<|>` 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 can and will 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, so
+why ever look back? Let's get started!
+
+
+The boilerplate required for both the nested and flat case
+
+```haskell
+{-# LANGUAGE GHC2021 #-}
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DerivingVia #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE OverloadedRecordDot #-}
+
+import Control.Exception (throwIO)
+import Control.Monad.Trans.Reader (ReaderT, runReaderT)
+import Network.Wai.Handler.Warp (run)
+import System.Environment (getArgs)
+import Data.Aeson (FromJSON (..), ToJSON (..))
+import GHC.Generics (Generic, Generically (..))
+import Data.List (sortOn)
+import Data.Text (Text)
+import Data.Foldable (find)
+
+import Servant
+import Servant.Client
+import Servant.Client.Generic
+import Servant.Server
+import Servant.Server.Generic
+```
+
+