diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,18 @@
 [The latest version of this document is on GitHub.](https://github.com/haskell-servant/servant/blob/master/servant-foreign/CHANGELOG.md)
 [Changelog for `servant` package contains significant entries for all core packages.](https://github.com/haskell-servant/servant/blob/master/servant/CHANGELOG.md)
 
+0.15.4
+------
+
+### Significant changes
+
+- Documentation improvements.
+
+### Other changes
+
+- Support GHC-9.0.1.
+- Bump `lens` and `hspec` dependencies.
+
 0.15.3
 ------
 
diff --git a/servant-foreign.cabal b/servant-foreign.cabal
--- a/servant-foreign.cabal
+++ b/servant-foreign.cabal
@@ -1,6 +1,6 @@
 cabal-version:       >=1.10
 name:                servant-foreign
-version:             0.15.3
+version:             0.15.4
 
 synopsis:            Helpers for generating clients for servant APIs in any programming language
 category:            Servant, Web
@@ -21,7 +21,7 @@
 maintainer:          haskell-servant-maintainers@googlegroups.com
 copyright:           2015-2019 Servant Contributors
 build-type:          Simple
-tested-with: GHC ==8.0.2 || ==8.2.2 || ==8.4.4 || ==8.6.5 || ==8.8.4 || ==8.10.2
+tested-with: GHC ==8.0.2 || ==8.2.2 || ==8.4.4 || ==8.6.5 || ==8.8.4 || ==8.10.2 || ==9.0.1
 
 extra-source-files:
   CHANGELOG.md
@@ -41,7 +41,7 @@
   --
   -- note: mtl lower bound is so low because of GHC-7.8
   build-depends:
-      base        >= 4.9     && < 4.15
+      base        >= 4.9     && < 4.16
     , text        >= 1.2.3.0 && < 1.3
 
   -- Servant dependencies
@@ -52,7 +52,7 @@
   -- Here can be exceptions if we really need features from the newer versions.
   build-depends:
       base-compat >= 0.10.5  && < 0.12
-    , lens        >= 4.17    && < 4.20
+    , lens        >= 4.17    && < 5.1
     , http-types  >= 0.12.2  && < 0.13
 
   hs-source-dirs:      src
@@ -74,8 +74,7 @@
 
   -- Additional dependencies
   build-depends:
-    hspec >= 2.6.0 && <2.8
-
+    hspec >= 2.6.0 && <2.9
   build-tool-depends:
-    hspec-discover:hspec-discover >=2.6.0 && <2.8
+    hspec-discover:hspec-discover >=2.6.0 && <2.9
   default-language:  Haskell2010
diff --git a/src/Servant/Foreign.hs b/src/Servant/Foreign.hs
--- a/src/Servant/Foreign.hs
+++ b/src/Servant/Foreign.hs
@@ -1,20 +1,32 @@
 -- | Generalizes all the data needed to make code generation work with
 -- arbitrary programming languages.
+--
+-- See documentation of 'HasForeignType' for a simple example. 'listFromAPI' returns a list of all your endpoints and their foreign types, given a mapping from Haskell types to foreign types (conventionally called `ftypes` below).
 module Servant.Foreign
-  ( ArgType(..)
-  , HeaderArg(..)
-  , QueryArg(..)
+  (
+  -- * Main API
+    listFromAPI
   , Req(..)
-  , ReqBodyContentType(..)
-  , Segment(..)
-  , SegmentType(..)
+  , defReq
+  , HasForeignType(..)
+  , GenerateList(..)
+  , HasForeign(..)
+  , NoTypes
+  -- * Subtypes of 'Req'
   , Url(..)
-    -- aliases
   , Path
+  , Segment(..)
+  , SegmentType(..)
+  , isCapture
+  , captureArg
+  , QueryArg(..)
+  , ArgType(..)
+  , HeaderArg(..)
   , Arg(..)
   , FunctionName(..)
+  , ReqBodyContentType(..)
   , PathSegment(..)
-    -- lenses
+    -- * Lenses
   , argName
   , argType
   , argPath
@@ -30,7 +42,7 @@
   , queryArgName
   , queryArgType
   , headerArg
-    -- prisms
+    -- * Prisms
   , _PathSegment
   , _HeaderArg
   , _ReplaceHeaderArg
@@ -39,16 +51,7 @@
   , _Normal
   , _Flag
   , _List
-    -- rest of it
-  , HasForeign(..)
-  , HasForeignType(..)
-  , GenerateList(..)
-  , NoTypes
-  , captureArg
-  , isCapture
-  , defReq
-  , listFromAPI
-    -- re-exports
+    -- * Re-exports
   , module Servant.API
   , module Servant.Foreign.Inflections
   ) where
diff --git a/src/Servant/Foreign/Inflections.hs b/src/Servant/Foreign/Inflections.hs
--- a/src/Servant/Foreign/Inflections.hs
+++ b/src/Servant/Foreign/Inflections.hs
@@ -20,20 +20,31 @@
                  (head, tail)
 import           Servant.Foreign.Internal
 
+-- | Simply concat each part of the FunctionName together.
+--
+-- @[ "get", "documents", "by", "id" ] → "getdocumentsbyid"@
+concatCase :: FunctionName -> Text
+concatCase = view concatCaseL
+
 concatCaseL :: Getter FunctionName Text
 concatCaseL = _FunctionName . to mconcat
 
--- | Function name builder that simply concat each part together
-concatCase :: FunctionName -> Text
-concatCase = view concatCaseL
+-- | Use the snake_case convention.
+-- Each part is separated by a single underscore character.
+--
+-- @[ "get", "documents", "by", "id" ] → "get_documents_by_id"@
+snakeCase :: FunctionName -> Text
+snakeCase = view snakeCaseL
 
 snakeCaseL :: Getter FunctionName Text
 snakeCaseL = _FunctionName . to (intercalate "_")
 
--- | Function name builder using the snake_case convention.
--- each part is separated by a single underscore character.
-snakeCase :: FunctionName -> Text
-snakeCase = view snakeCaseL
+-- | Use the camelCase convention.
+-- The first part is lower case, every other part starts with an upper case character.
+--
+-- @[ "get", "documents", "by", "id" ] → "getDocumentsById"@
+camelCase :: FunctionName -> Text
+camelCase = view camelCaseL
 
 camelCaseL :: Getter FunctionName Text
 camelCaseL = _FunctionName . to convert
@@ -42,8 +53,3 @@
     convert (p:ps) = mconcat $ p : map capitalize ps
     capitalize ""   = ""
     capitalize name = C.toUpper (head name) `cons` tail name
-
--- | Function name builder using the CamelCase convention.
--- each part begins with an upper case character.
-camelCase :: FunctionName -> Text
-camelCase = view camelCaseL
diff --git a/src/Servant/Foreign/Internal.hs b/src/Servant/Foreign/Internal.hs
--- a/src/Servant/Foreign/Internal.hs
+++ b/src/Servant/Foreign/Internal.hs
@@ -13,8 +13,6 @@
 {-# LANGUAGE TypeOperators              #-}
 {-# LANGUAGE UndecidableInstances       #-}
 
--- | Generalizes all the data needed to make code generation work with
--- arbitrary programming languages.
 module Servant.Foreign.Internal where
 
 import           Prelude ()
@@ -40,55 +38,75 @@
                  (RequiredArgument)
 import           Servant.API.TypeLevel
 
+-- | Canonical name of the endpoint, can be used to generate a function name.
+--
+-- You can use the functions in "Servant.Foreign.Inflections", like 'Servant.Foreign.Inflections.camelCase' to transform to `Text`.
 newtype FunctionName = FunctionName { unFunctionName :: [Text] }
   deriving (Data, Show, Eq, Semigroup, Monoid, Typeable)
 
 makePrisms ''FunctionName
 
+-- | See documentation of 'Arg'
 newtype PathSegment = PathSegment { unPathSegment :: Text }
   deriving (Data, Show, Eq, IsString, Semigroup, Monoid, Typeable)
 
 makePrisms ''PathSegment
 
-data Arg f = Arg
+-- | Maps a name to the foreign type that belongs to the annotated value.
+--
+-- Used for header args, query args, and capture args.
+data Arg ftype = Arg
   { _argName :: PathSegment
-  , _argType :: f }
+  -- ^ The name to be captured.
+  --
+  -- Only for capture args it really denotes a path segment.
+  , _argType :: ftype
+  -- ^ Foreign type the associated value will have
+  }
   deriving (Data, Eq, Show, Typeable)
 
 makeLenses ''Arg
 
-argPath :: Getter (Arg f) Text
+argPath :: Getter (Arg ftype) Text
 argPath = argName . _PathSegment
 
-data SegmentType f
+data SegmentType ftype
   = Static PathSegment
-    -- ^ a static path segment. like "/foo"
-  | Cap (Arg f)
-    -- ^ a capture. like "/:userid"
+    -- ^ Static path segment.
+    --
+    -- @"foo\/bar\/baz"@
+    --
+    -- contains the static segments @"foo"@, @"bar"@ and @"baz"@.
+  | Cap (Arg ftype)
+    -- ^ A capture.
+    --
+    -- @"user\/{userid}\/name"@
+    --
+    -- would capture the arg @userid@ with type @ftype@.
   deriving (Data, Eq, Show, Typeable)
 
 makePrisms ''SegmentType
 
-newtype Segment f = Segment { unSegment :: SegmentType f }
+-- | A part of the Url’s path.
+newtype Segment ftype = Segment { unSegment :: SegmentType ftype }
   deriving (Data, Eq, Show, Typeable)
 
 makePrisms ''Segment
 
-isCapture :: Segment f -> Bool
+-- | Whether a segment is a 'Cap'.
+isCapture :: Segment ftype -> Bool
 isCapture (Segment (Cap _)) = True
 isCapture                _  = False
 
-captureArg :: Segment f -> Arg f
+-- | Crashing Arg extraction from segment, TODO: remove
+captureArg :: Segment ftype -> Arg ftype
 captureArg (Segment (Cap s)) = s
 captureArg                 _ = error "captureArg called on non capture"
 
-type Path f = [Segment f]
-
-newtype Frag f = Frag { unFragment :: Arg f }
-  deriving (Data, Eq, Show, Typeable)
-
-makePrisms ''Frag
+-- TODO: remove, unnecessary indirection
+type Path ftype = [Segment ftype]
 
+-- | Type of a 'QueryArg'.
 data ArgType
   = Normal
   | Flag
@@ -97,18 +115,41 @@
 
 makePrisms ''ArgType
 
-data QueryArg f = QueryArg
-  { _queryArgName :: Arg f
+-- | Url Query argument.
+--
+-- Urls can contain query arguments, which is a list of key-value pairs.
+-- In a typical url, query arguments look like this:
+--
+-- @?foo=bar&alist[]=el1&alist[]=el2&aflag@
+--
+-- Each pair can be
+--
+-- * @?foo=bar@: a plain key-val pair, either optional or required ('QueryParam')
+-- * @?aflag@: a flag (no value, implicitly Bool with default `false` if it’s missing) ('QueryFlag')
+-- * @?alist[]=el1&alist[]=el2@: list of values ('QueryParams')
+--
+-- @_queryArgType@ will be set accordingly.
+--
+-- For the plain key-val pairs ('QueryParam'), @_queryArgName@’s @ftype@ will be wrapped in a @Maybe@ if the argument is optional.
+data QueryArg ftype = QueryArg
+  { _queryArgName :: Arg ftype
+  -- ^ Name and foreign type of the argument. Will be wrapped in `Maybe` if the query is optional and in a `[]` if the query is a list
   , _queryArgType :: ArgType
+  -- ^ one of normal/plain, list or flag
   }
   deriving (Data, Eq, Show, Typeable)
 
 makeLenses ''QueryArg
 
-data HeaderArg f = HeaderArg
-  { _headerArg :: Arg f }
+data HeaderArg ftype =
+  -- | The name of the header and the foreign type of its value.
+  HeaderArg
+  { _headerArg :: Arg ftype }
+  -- | Unused, will never be set.
+  --
+  -- TODO: remove
   | ReplaceHeaderArg
-  { _headerArg     :: Arg f
+  { _headerArg     :: Arg ftype
   , _headerPattern :: Text
   }
   deriving (Data, Eq, Show, Typeable)
@@ -117,29 +158,71 @@
 
 makePrisms ''HeaderArg
 
-data Url f = Url
-  { _path     :: Path f
-  , _queryStr :: [QueryArg f]
-  , _frag     :: Maybe f
+-- | Full endpoint url, with all captures and parameters
+data Url ftype = Url
+  { _path     :: Path ftype
+  -- ^ Url path, list of either static segments or captures
+  --
+  -- @"foo\/{id}\/bar"@
+  , _queryStr :: [QueryArg ftype]
+  -- ^ List of query args
+  --
+  -- @"?foo=bar&a=b"@
+  , _frag     :: Maybe ftype
+  -- ^ Url fragment.
+  --
+  -- Not sent to the HTTP server, so only useful for frontend matters (e.g. inter-page linking).
+  --
+  -- @#fragmentText@
   }
   deriving (Data, Eq, Show, Typeable)
 
-defUrl :: Url f
+defUrl :: Url ftype
 defUrl = Url [] [] Nothing
 
 makeLenses ''Url
 
+-- | See documentation of '_reqBodyContentType'
 data ReqBodyContentType = ReqBodyJSON | ReqBodyMultipart
   deriving (Data, Eq, Show, Read)
 
-data Req f = Req
-  { _reqUrl             :: Url f
+-- | Full description of an endpoint in your API, generated by 'listFromAPI'. It should give you all the information needed to generate foreign language bindings.
+--
+-- Every field containing @ftype@ will use the foreign type mapping specified via 'HasForeignType' (see its docstring on how to set that up).
+--
+-- See https://docs.servant.dev/en/stable/tutorial/ApiType.html for accessible documentation of the possible content of an endpoint.
+data Req ftype = Req
+  { _reqUrl             :: Url ftype
+  -- ^ Full list of URL segments, including captures
   , _reqMethod          :: HTTP.Method
-  , _reqHeaders         :: [HeaderArg f]
-  , _reqBody            :: Maybe f
-  , _reqReturnType      :: Maybe f
+  -- ^ @\"GET\"@\/@\"POST\"@\/@\"PUT\"@\/…
+  , _reqHeaders         :: [HeaderArg ftype]
+  -- ^ Headers required by this endpoint, with their type
+  , _reqBody            :: Maybe ftype
+  -- ^ Foreign type of the expected request body ('ReqBody'), if any
+  , _reqReturnType      :: Maybe ftype
+  -- ^ The foreign type of the response, if any
   , _reqFuncName        :: FunctionName
+  -- ^ The URL segments rendered in a way that they can be easily concatenated into a canonical function name
   , _reqBodyContentType :: ReqBodyContentType
+  -- ^ The content type the request body is transferred as.
+  --
+  -- This is a severe limitation of @servant-foreign@ currently,
+  -- as we only allow the content type to be `JSON`
+  -- no user-defined content types. ('ReqBodyMultipart' is not
+  -- actually implemented.)
+  --
+  -- Thus, any routes looking like this will work:
+  --
+  -- @"foo" :> Get '[JSON] Foo@
+  --
+  -- while routes like
+  --
+  -- @"foo" :> Get '[MyFancyContentType] Foo@
+  --
+  -- will fail with an error like
+  --
+  -- @• JSON expected in list '[MyFancyContentType]@
   }
   deriving (Data, Eq, Show, Typeable)
 
@@ -183,11 +266,16 @@
 class HasForeignType lang ftype a where
   typeFor :: Proxy lang -> Proxy ftype -> Proxy a -> ftype
 
+-- | The language definition without any foreign types. It can be used for dynamic languages which do not /do/ type annotations.
 data NoTypes
 
-instance HasForeignType NoTypes NoContent ftype where
+-- | Use if the foreign language does not have any types.
+instance HasForeignType NoTypes NoContent a where
   typeFor _ _ _ = NoContent
 
+-- | Implementation of the Servant framework types.
+--
+-- Relevant instances: Everything containing 'HasForeignType'.
 class HasForeign lang ftype (api :: *) where
   type Foreign ftype api :: *
   foreignFor :: Proxy lang -> Proxy ftype -> Proxy api -> Req ftype -> Foreign ftype api
