diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,7 @@
+# Changelog for servant-to-elm
+
+## Unreleased changes
+
+## 0.1.0.0
+
+- Initial release
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright Olle Fredriksson (c) 2019
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+
+    * Neither the name of Olle Fredriksson nor the names of other
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,140 @@
+# servant-to-elm
+
+This is a library for generating Elm client libraries from Servant API
+definitions.
+
+See [haskell-to-elm](https://github.com/folq/haskell-to-elm) for background
+information and a more elaborate motivation.
+
+## Basic usage
+
+Given a Servant API like the following
+
+```haskell
+type UserAPI
+  = "user" :> Get '[JSON] User
+  :<|> "user" :> ReqBody '[JSON] User :> Post '[JSON] NoContent
+```
+
+we can generate the Elm code for making requests against it as follows:
+
+```haskell
+main :: IO ()
+main = do
+  let
+    definitions =
+      map (elmEndpointDefinition "Config.urlBase" ["Api"]) (elmEndpoints @UserAPI)
+      <> jsonDefinitions @User
+
+    modules =
+      Pretty.modules definitions
+
+  forM_ (HashMap.toList modules) $ \(_moduleName, contents) ->
+    print contents
+```
+
+Running `main` prints:
+
+```elm
+module Api exposing (..)
+
+import Api.User
+import Config
+import Http
+import Json.Decode
+import NoContent
+
+
+getUser : Cmd (Result (Http.Error , Maybe { metadata : Http.Metadata
+    , body : String }) Api.User.User)
+getUser =
+    Http.request { method = "GET"
+    , headers = []
+    , url = String.join "/" [Config.urlBase, "user"]
+    , body = Http.emptyBody
+    , expect = Http.expectStringResponse identity (\a -> case a of
+        Http.BadUrl_ b ->
+            Err (Http.BadUrl b , Nothing)
+
+        Http.Timeout_ ->
+            Err (Http.Timeout , Nothing)
+
+        Http.NetworkError_ ->
+            Err (Http.NetworkError , Nothing)
+
+        Http.BadStatus_ b c ->
+            Err (Http.BadStatus b.statusCode , Just { metadata = b, body = c })
+
+        Http.GoodStatus_ b c ->
+            Result.mapError (\d -> (Http.BadBody (Json.Decode.errorToString d) , Just { metadata = b
+            , body = c })) (Json.Decode.decodeString Api.User.decoder c))
+    , timeout = Nothing
+    , tracker = Nothing }
+
+
+postUser : Api.User.User -> Cmd (Result (Http.Error , Maybe { metadata : Http.Metadata
+    , body : String }) NoContent.NoContent)
+postUser a =
+    Http.request { method = "POST"
+    , headers = []
+    , url = String.join "/" [Config.urlBase, "user"]
+    , body = Http.jsonBody (Api.User.encoder a)
+    , expect = Http.expectStringResponse identity (\b -> case b of
+        Http.BadUrl_ c ->
+            Err (Http.BadUrl c , Nothing)
+
+        Http.Timeout_ ->
+            Err (Http.Timeout , Nothing)
+
+        Http.NetworkError_ ->
+            Err (Http.NetworkError , Nothing)
+
+        Http.BadStatus_ c d ->
+            Err (Http.BadStatus c.statusCode , Just { metadata = c, body = d })
+
+        Http.GoodStatus_ c d ->
+            if d == "" then
+                Ok NoContent.NoContent
+
+            else
+                Err (Http.BadBody "Expected the response body to be empty" , Just { metadata = c
+                , body = d }))
+    , timeout = Nothing
+    , tracker = Nothing }
+module Api.User exposing (..)
+
+import Json.Decode
+import Json.Decode.Pipeline
+import Json.Encode
+
+
+type alias User =
+    { name : String, age : Int }
+
+
+encoder : User -> Json.Encode.Value
+encoder a =
+    Json.Encode.object [ ("name" , Json.Encode.string a.name)
+    , ("age" , Json.Encode.int a.age) ]
+
+
+decoder : Json.Decode.Decoder User
+decoder =
+    Json.Decode.succeed User |>
+    Json.Decode.Pipeline.required "name" Json.Decode.string |>
+    Json.Decode.Pipeline.required "age" Json.Decode.int
+```
+
+In an actual project we would be writing the code to disk instead of printing it.
+
+See [this file](examples/UserAPI.hs) for the full code with imports.
+
+## Related projects
+
+Libraries that use or are used by servant-to-elm:
+- [haskell-to-elm](https://github.com/folq/haskell-to-elm) generates Elm types and JSON encoders and decoders from Haskell types.
+- [elm-syntax](https://github.com/folq/elm-syntax) defines Haskell ASTs for Elm's syntax, and lets us pretty-print it.
+- [haskell-to-elm-test](https://github.com/folq/haskell-to-elm-test) does end-to-end testing of this library.
+
+Others:
+- [servant-elm](http://hackage.haskell.org/package/servant-elm)
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/examples/UserAPI.hs b/examples/UserAPI.hs
new file mode 100644
--- /dev/null
+++ b/examples/UserAPI.hs
@@ -0,0 +1,62 @@
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE NoImplicitPrelude #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE TypeApplications #-}
+{-# LANGUAGE TypeOperators #-}
+module UserAPI where
+
+import Protolude
+
+import qualified Data.Aeson as Aeson
+import qualified Data.HashMap.Lazy as HashMap
+import qualified Generics.SOP as SOP
+import Servant.API
+
+import qualified Language.Elm.Pretty as Pretty
+import Language.Haskell.To.Elm
+import Servant.To.Elm
+
+data User = User
+  { name :: Text
+  , age :: Int
+  } deriving
+    ( Generic
+    , Aeson.ToJSON
+    , SOP.Generic
+    , SOP.HasDatatypeInfo
+    , HasElmDecoder Aeson.Value
+    , HasElmEncoder Aeson.Value
+    , HasElmType
+    )
+
+instance HasElmDefinition User where
+  elmDefinition =
+    deriveElmTypeDefinition @User defaultOptions "Api.User.User"
+
+instance HasElmDecoderDefinition Aeson.Value User where
+  elmDecoderDefinition =
+    deriveElmJSONDecoder @User defaultOptions Aeson.defaultOptions "Api.User.decoder"
+
+instance HasElmEncoderDefinition Aeson.Value User where
+  elmEncoderDefinition =
+    deriveElmJSONEncoder @User defaultOptions Aeson.defaultOptions "Api.User.encoder"
+
+type UserAPI
+  = "user" :> Get '[JSON] User
+  :<|> "user" :> ReqBody '[JSON] User :> Post '[JSON] NoContent
+
+main :: IO ()
+main = do
+  let
+    definitions =
+      map (elmEndpointDefinition "Config.urlBase" ["Api"]) (elmEndpoints @UserAPI)
+      <> jsonDefinitions @User
+
+    modules =
+      Pretty.modules definitions
+
+  forM_ (HashMap.toList modules) $ \(_moduleName, contents) ->
+    print contents
diff --git a/servant-to-elm.cabal b/servant-to-elm.cabal
new file mode 100644
--- /dev/null
+++ b/servant-to-elm.cabal
@@ -0,0 +1,98 @@
+cabal-version: 1.12
+
+-- This file has been generated from package.yaml by hpack version 0.31.2.
+--
+-- see: https://github.com/sol/hpack
+--
+-- hash: a72d33a5b5a0f94d44e7e4e23970669fe65894da52c59170b08a0c87725c898e
+
+name:           servant-to-elm
+version:        0.1.0.0
+synopsis:       Automatically generate Elm clients for Servant APIs
+description:    Please see the README on GitHub at <https://github.com/folq/servant-to-elm#readme>
+category:       Servant, API, Elm, Compiler
+homepage:       https://github.com/folq/servant-to-elm#readme
+bug-reports:    https://github.com/folq/servant-to-elm/issues
+author:         Olle Fredriksson
+maintainer:     fredriksson.olle@gmail.com
+copyright:      2019 Olle Fredriksson
+license:        BSD3
+license-file:   LICENSE
+build-type:     Simple
+extra-source-files:
+    README.md
+    CHANGELOG.md
+
+source-repository head
+  type: git
+  location: https://github.com/folq/servant-to-elm
+
+flag examples
+  description: Build examples
+  manual: True
+  default: False
+
+library
+  exposed-modules:
+      Servant.To.Elm
+  other-modules:
+      Paths_servant_to_elm
+  hs-source-dirs:
+      src
+  ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -Wtabs -funbox-strict-fields
+  build-depends:
+      aeson >=1.4.0
+    , base >=4.7 && <5
+    , bound >=2.0.0
+    , elm-syntax >=0.1.0 && <0.1.1
+    , haskell-to-elm >=0.1.0 && <0.1.1
+    , http-types >=0.12.0
+    , protolude >=0.2.3
+    , servant >=0.16.0
+    , text >=1.2.0
+  default-language: Haskell2010
+
+executable user-example
+  main-is: UserAPI.hs
+  other-modules:
+      Paths_servant_to_elm
+  hs-source-dirs:
+      examples
+  ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -Wtabs -funbox-strict-fields
+  build-depends:
+      aeson >=1.4.0
+    , base >=4.7 && <5
+    , bound >=2.0.0
+    , elm-syntax >=0.1.0 && <0.1.1
+    , generics-sop
+    , haskell-to-elm >=0.1.0 && <0.1.1
+    , http-types >=0.12.0
+    , protolude >=0.2.3
+    , servant
+    , servant-to-elm
+    , text >=1.2.0
+    , unordered-containers
+  if !flag(examples)
+    buildable: False
+  default-language: Haskell2010
+
+test-suite servant-to-elm-test
+  type: exitcode-stdio-1.0
+  main-is: Spec.hs
+  other-modules:
+      Paths_servant_to_elm
+  hs-source-dirs:
+      test
+  ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -Wtabs -funbox-strict-fields -threaded -rtsopts -with-rtsopts=-N
+  build-depends:
+      aeson >=1.4.0
+    , base >=4.7 && <5
+    , bound >=2.0.0
+    , elm-syntax >=0.1.0 && <0.1.1
+    , haskell-to-elm >=0.1.0 && <0.1.1
+    , http-types >=0.12.0
+    , protolude >=0.2.3
+    , servant >=0.16.0
+    , servant-to-elm
+    , text >=1.2.0
+  default-language: Haskell2010
diff --git a/src/Servant/To/Elm.hs b/src/Servant/To/Elm.hs
new file mode 100644
--- /dev/null
+++ b/src/Servant/To/Elm.hs
@@ -0,0 +1,613 @@
+{-# LANGUAGE AllowAmbiguousTypes #-}
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE GADTs #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE NoImplicitPrelude #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE PolyKinds #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TypeApplications #-}
+{-# LANGUAGE TypeOperators #-}
+{-# LANGUAGE UndecidableInstances #-}
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+module Servant.To.Elm where
+
+import Protolude hiding (Type, functionName, moduleName)
+
+import qualified Bound
+import qualified Data.Aeson as Aeson
+import qualified Data.Char as Char
+import qualified Data.Text as Text
+import qualified Network.HTTP.Types as HTTP
+import Servant.API ((:<|>), (:>))
+import qualified Servant.API as Servant
+import qualified Servant.API.Modifiers as Servant
+
+import Language.Elm.Definition (Definition)
+import qualified Language.Elm.Definition as Definition
+import Language.Elm.Expression (Expression)
+import qualified Language.Elm.Expression as Expression
+import qualified Language.Elm.Name as Name
+import qualified Language.Elm.Pattern as Pattern
+import Language.Elm.Type (Type)
+import qualified Language.Elm.Type as Type
+import Language.Haskell.To.Elm
+
+-- | Generate an Elm function for making a request to a Servant endpoint.
+elmEndpointDefinition
+  :: Expression Void -- ^ The URL base of the endpoint
+  -> Name.Module -- ^ The module that the function should be generated into
+  -> Endpoint -- ^ A description of the endpoint
+  -> Definition
+elmEndpointDefinition urlBase moduleName endpoint =
+  Definition.Constant
+    (Name.Qualified moduleName functionName)
+    elmTypeSig
+    (panic "expression not closed" <$> lambdaArgs argNames elmLambdaBody)
+  where
+    functionName =
+      case _functionName endpoint of
+        [] ->
+          ""
+
+        firstPart:rest ->
+          firstPart <> foldMap capitalise rest
+
+    capitalise s =
+      case Text.uncons s of
+        Nothing ->
+          ""
+
+        Just (c, s') ->
+          Text.cons (Char.toUpper c) s'
+
+    elmTypeSig :: Type Void
+    elmTypeSig =
+      Type.funs
+        (concat
+          [ [ _encodedType arg
+            | (_, arg, _) <- _headers endpoint
+            ]
+          , [ _encodedType arg
+            | Capture _ (_, arg) <- numberedPathSegments
+            ]
+          , [ case type_ of
+                Required ->
+                  vacuous $ _encodedType arg
+
+                Optional ->
+                  vacuous $ _encodedType arg
+
+                Flag ->
+                  vacuous $ _encodedType arg
+
+                List ->
+                  vacuous $ Type.App "List.List" $ _encodedType arg
+            | (_, type_, arg) <- _queryString $ _url endpoint
+            ]
+          , [ _encodedType body
+            | Just body <- [_body endpoint]
+            ]
+          ]
+        )
+        elmReturnType
+
+    elmReturnType =
+      let
+        type_ =
+          maybe "Basics.()" _decodedType $ _returnType endpoint
+      in
+      Type.App
+        "Cmd.Cmd"
+        (Type.apps
+          "Result.Result"
+          [Type.tuple "Http.Error" (Type.App "Maybe.Maybe" $ Type.Record [("metadata", "Http.Metadata"), ("body", "String.String")]), type_]
+        )
+
+    elmReturnDecoder =
+      case _returnType endpoint of
+        Nothing ->
+          panic "elmRequest: No return type" -- TODO?
+
+        Just ret ->
+          vacuous $ _decoder ret
+
+    numberedPathSegments =
+      go 0 $ _path $ _url endpoint
+      where
+        go !i segments =
+          case segments of
+            [] ->
+              []
+
+            Static p:segments' ->
+              Static p : go i segments'
+
+            Capture str arg:segments' ->
+              Capture str (i, arg) : go (i + 1) segments'
+
+    argNames =
+      concat
+      [ [ headerArgName i
+        | (i, _) <- zip [0..] $ _headers endpoint
+        ]
+      , [ capturedArgName i
+        | Capture _ (i, _) <- numberedPathSegments
+        ]
+      , [ paramArgName i
+        | (i, _) <- zip [0..] $ _queryString $ _url endpoint
+        ]
+      , [ bodyArgName
+        | Just _ <- [_body endpoint]
+        ]
+      ]
+
+    lambdaArgs :: [Text] -> Expression Text -> Expression Text
+    lambdaArgs args rhs =
+      case args of
+        [] ->
+          rhs
+
+        arg:args' ->
+          Expression.Lam $ Bound.abstract1 arg $ lambdaArgs args' rhs
+
+    elmLambdaBody :: Expression Text
+    elmLambdaBody =
+      Expression.App
+        "Http.request"
+        (Expression.Record
+          [ ("method", Expression.String $ toS $ _method endpoint)
+          , ("headers", elmHeaders)
+          , ("url", elmUrl)
+          , ("body", elmBody)
+          , ("expect", elmExpect)
+          , ("timeout", "Maybe.Nothing")
+          , ("tracker", "Maybe.Nothing")
+          ]
+        )
+
+    elmParams =
+      [ case type_ of
+        Required ->
+          Expression.List
+            [Expression.String (name <> "=") Expression.++ encode (pure $ paramArgName i)]
+
+        Optional ->
+          Expression.apps
+            "Maybe.Extra.unwrap"
+            [ Expression.List []
+            , "List.singleton" Expression.<< Expression.App "Basics.++" (Expression.String $ name <> "=")
+            , encode $ pure $ paramArgName i
+            ]
+
+        Flag ->
+          Expression.If
+            (pure $ paramArgName i)
+            (Expression.List [Expression.String name])
+            (Expression.List [])
+
+        List ->
+          Expression.apps
+            "List.map"
+            [ Expression.App "Basics.++" (Expression.String (name <> "[]=")) Expression.<< encoder
+            , pure $ paramArgName i
+            ]
+      | (i, (name, type_, arg)) <- zip [0..] $ _queryString $ _url endpoint
+      , let
+          encoder =
+            vacuous $ _encoder arg
+
+          encode =
+            Expression.App encoder
+      ]
+
+    elmUrl =
+      case elmParams of
+        [] ->
+          withoutParams
+
+        [elmParams'] ->
+          withParams elmParams'
+
+        _ ->
+          withParams (Expression.App "List.concat" $ Expression.List elmParams)
+
+      where
+        withoutParams =
+          Expression.apps
+            "String.join"
+            [ Expression.String "/"
+            , Expression.List $ vacuous urlBase : fmap elmPathSegment numberedPathSegments
+            ]
+
+        withParams params =
+          withoutParams Expression.++
+            Expression.Case params
+              [ (Pattern.List [], Bound.toScope $ Expression.String "")
+              , ( Pattern.Var 0
+                , Bound.toScope $ Expression.String "?" Expression.++ Expression.apps "String.join" [Expression.String "&", pure $ Bound.B 0]
+                )
+              ]
+
+
+    elmHeaders =
+      let
+        headerDecoder i name arg  =
+          Expression.apps
+            "Http.header"
+            [ Expression.String name
+            , Expression.App
+              (vacuous $ _encoder arg)
+              (pure $ headerArgName i)
+            ]
+
+        optionalHeaderDecoder i name arg =
+          Expression.apps
+            "Maybe.map"
+            [ Expression.App
+              "Http.header"
+              (Expression.String name)
+            , Expression.App
+              (vacuous $ _encoder arg)
+              (pure $ headerArgName i)
+            ]
+      in
+      case _headers endpoint of
+        [] ->
+          Expression.List []
+
+        _
+          | all (\(_, _, required) -> required) (_headers endpoint) ->
+          Expression.List
+            [ headerDecoder i name arg
+            | (i, (name, arg, _)) <- zip [0..] $ _headers endpoint
+            ]
+
+        _ ->
+          Expression.apps "List.filterMap"
+          [ "Basics.identity"
+          , Expression.List
+              [ if required then
+                  Expression.App "Maybe.Just" $ headerDecoder i name arg
+
+                else
+                  optionalHeaderDecoder i name arg
+              | (i, (name, arg, required)) <- zip [0..] $ _headers endpoint
+              ]
+          ]
+
+
+    elmBody =
+      case _body endpoint of
+        Nothing ->
+          "Http.emptyBody"
+
+        Just body ->
+          Expression.App
+            "Http.jsonBody"
+            (Expression.App (vacuous $ _encoder body) $ pure bodyArgName)
+
+    elmExpect =
+      Expression.apps
+        "Http.expectStringResponse"
+        [ "Basics.identity"
+        , Expression.Lam $ Bound.toScope $
+            Expression.Case (pure $ Bound.B ())
+            [ ( Pattern.Con "Http.BadUrl_" [Pattern.Var 0]
+              , Bound.toScope $
+                Expression.App "Result.Err" $
+                Expression.tuple (Expression.App "Http.BadUrl" $ pure (Bound.B 0)) "Maybe.Nothing"
+              )
+            , ( Pattern.Con "Http.Timeout_" []
+              , Bound.toScope $
+                Expression.App "Result.Err" $
+                Expression.tuple "Http.Timeout" "Maybe.Nothing"
+              )
+            , ( Pattern.Con "Http.NetworkError_" []
+              , Bound.toScope $
+                Expression.App "Result.Err" $
+                Expression.tuple "Http.NetworkError" "Maybe.Nothing"
+              )
+            , ( Pattern.Con "Http.BadStatus_" [Pattern.Var 0, Pattern.Var 1]
+              , Bound.toScope $
+                Expression.App "Result.Err" $
+                Expression.tuple
+                  (Expression.App "Http.BadStatus" (Expression.App (Expression.Proj "statusCode") $ pure $ Bound.B 0))
+                  (Expression.App "Maybe.Just" $ Expression.Record [("metadata", pure $ Bound.B 0), ("body", pure $ Bound.B 1)])
+              )
+            , ( Pattern.Con "Http.GoodStatus_" [Pattern.Var 0, Pattern.Var 1]
+              , Bound.toScope $
+                if fmap _decodedType (_returnType endpoint) == Just (elmType @Servant.NoContent) then
+                  Expression.If (Expression.apps ("Basics.==") [pure $ Bound.B 1, Expression.String ""])
+                    (Expression.App "Result.Ok" "NoContent.NoContent")
+                    (Expression.App "Result.Err" $
+                      Expression.tuple
+                        (Expression.App "Http.BadBody" $ Expression.String "Expected the response body to be empty")
+                        (Expression.App "Maybe.Just" $ Expression.Record [("metadata", pure $ Bound.B 0), ("body", pure $ Bound.B 1)])
+                    )
+
+                else
+                  Expression.apps "Result.mapError"
+                    [ Expression.Lam $ Bound.toScope $
+                      Expression.tuple
+                        (Expression.App "Http.BadBody" $
+                          Expression.App "Json.Decode.errorToString" $
+                          pure $ Bound.B ()
+                        )
+                        (Expression.App "Maybe.Just" $ Expression.Record [("metadata", pure $ Bound.F $ Bound.B 0), ("body", pure $ Bound.F $ Bound.B 1)])
+                    , Expression.apps "Json.Decode.decodeString" [elmReturnDecoder, pure $ Bound.B 1]
+                    ]
+              )
+            ]
+        ]
+
+    elmPathSegment pathSegment =
+      case pathSegment of
+        Static s ->
+          Expression.String s
+
+        Capture _ (i, arg) ->
+          Expression.App
+            (vacuous $ _encoder arg)
+            (pure $ capturedArgName i)
+
+    bodyArgName :: Text
+    bodyArgName =
+      "body"
+
+    headerArgName :: Int -> Text
+    headerArgName i =
+      "header" <> show i
+
+    capturedArgName :: Int -> Text
+    capturedArgName i =
+      "capture" <> show i
+
+    paramArgName :: Int -> Text
+    paramArgName i =
+      "param" <> show i
+
+-------------------------------------------------------------------------------
+-- * Endpoints
+
+-- | @'HasElmEndpoints' api@ means that the Servant API @api@ can be converted
+-- to a list of 'Endpoint's, which contains the information we need to generate
+-- an Elm client library for the API.
+class HasElmEndpoints api where
+  elmEndpoints' :: Endpoint -> [Endpoint]
+
+-- | Convert an API to a list of Elm endpoint descriptors, 'Endpoint'.
+--
+-- Usage: @'elmEndpoints' \@MyAPI@
+elmEndpoints :: forall api. HasElmEndpoints api => [Endpoint]
+elmEndpoints = elmEndpoints' @api Endpoint
+  { _url = URL
+    { _path = []
+    , _queryString = []
+    }
+  , _method = "GET"
+  , _headers = []
+  , _body = Nothing
+  , _returnType = Nothing
+  , _functionName = []
+  }
+
+-- | Contains the information we need about an endpoint to generate an Elm
+-- definition that calls it.
+data Endpoint = Endpoint
+  { _url :: URL
+  , _method :: HTTP.Method
+  , _headers :: [(Text, Encoder, Bool)]
+  , _body :: Maybe Encoder
+  , _returnType :: Maybe Decoder
+  , _functionName :: [Text]
+  }
+
+data PathSegment e
+  = Static Text
+  | Capture Text e
+  deriving (Show)
+
+data QueryParamType
+  = Required
+  | Optional
+  | Flag
+  | List
+  deriving (Show)
+
+data URL = URL
+  { _path :: [PathSegment Encoder]
+  , _queryString :: [(Text, QueryParamType, Encoder)]
+  }
+  deriving (Show)
+
+data Encoder = Encoder { _encoder :: Expression Void, _encodedType :: Type Void }
+  deriving (Show)
+data Decoder = Decoder { _decoder :: Expression Void, _decodedType :: Type Void }
+  deriving (Show)
+
+makeEncoder :: forall value a. HasElmEncoder value a => Encoder
+makeEncoder = Encoder (elmEncoder @value @a) (elmType @a)
+
+makeDecoder :: forall value a. HasElmDecoder value a => Decoder
+makeDecoder = Decoder (elmDecoder @value @a) (elmType @a)
+
+instance HasElmEndpoints Servant.EmptyAPI where
+  elmEndpoints' _ = []
+
+instance (HasElmEndpoints a, HasElmEndpoints b) => HasElmEndpoints (a :<|> b) where
+  elmEndpoints' prefix =
+    elmEndpoints' @a prefix <> elmEndpoints' @b prefix
+
+instance (KnownSymbol symbol, HasElmEncoder Text a, HasElmEndpoints api)
+  => HasElmEndpoints (Servant.Capture' mods symbol a :> api) where
+    elmEndpoints' prefix =
+      elmEndpoints' @api prefix
+        { _url = (_url prefix)
+          { _path = _path (_url prefix) <> [Capture str $ makeEncoder @Text @a]
+          }
+        , _functionName = _functionName prefix <> ["by", str]
+        }
+      where
+        str =
+          toS $ symbolVal $ Proxy @symbol
+
+instance (KnownSymbol symbol, HasElmEncoder Text a, HasElmEndpoints api)
+  => HasElmEndpoints (Servant.CaptureAll symbol a :> api) where
+    elmEndpoints' prefix =
+      elmEndpoints' @api prefix
+        { _url = (_url prefix)
+          { _path = _path (_url prefix) <> [Capture str $ makeEncoder @Text @a]
+          }
+        , _functionName = _functionName prefix <> ["by", str]
+        }
+      where
+        str =
+          toS $ symbolVal $ Proxy @symbol
+
+instance (Servant.ReflectMethod method, HasElmDecoder Aeson.Value a, list ~ '[Servant.JSON])
+  => HasElmEndpoints (Servant.Verb method status list a) where
+    elmEndpoints' prefix =
+      [ prefix
+        { _method = method
+        , _returnType = Just $ makeDecoder @Aeson.Value @a
+        , _functionName = Text.toLower (toS method) : _functionName prefix
+        }
+      ]
+      where
+        method =
+          Servant.reflectMethod $ Proxy @method
+
+instance
+  ( Servant.SBoolI (Servant.FoldRequired mods)
+  , KnownSymbol symbol
+  , HasElmEncoder (Servant.RequiredArgument mods Text) (Servant.RequiredArgument mods a)
+  , HasElmEndpoints api
+  ) => HasElmEndpoints (Servant.Header' mods symbol a :> api) where
+    elmEndpoints' prefix =
+      elmEndpoints' @api prefix
+        { _headers = _headers prefix <>
+          [ ( toS $ symbolVal $ Proxy @symbol
+            , makeEncoder @(Servant.RequiredArgument mods Text) @(Servant.RequiredArgument mods a)
+            , case Servant.sbool @(Servant.FoldRequired mods) of
+                Servant.STrue ->
+                  True
+
+                Servant.SFalse ->
+                  False
+            )
+          ]
+        }
+
+instance
+  ( Servant.SBoolI (Servant.FoldRequired mods)
+  , KnownSymbol symbol
+  , HasElmEncoder (Servant.RequiredArgument mods Text) (Servant.RequiredArgument mods a)
+  , HasElmEndpoints api
+  ) => HasElmEndpoints (Servant.QueryParam' mods symbol a :> api) where
+    elmEndpoints' prefix =
+      elmEndpoints' @api prefix
+        { _url = (_url prefix)
+          { _queryString =
+            _queryString (_url prefix) <>
+            [ ( toS $ symbolVal $ Proxy @symbol
+              , case Servant.sbool @(Servant.FoldRequired mods) of
+                  Servant.STrue ->
+                    Required
+
+                  Servant.SFalse ->
+                    Optional
+              , makeEncoder @(Servant.RequiredArgument mods Text) @(Servant.RequiredArgument mods a)
+              )
+            ]
+          }
+        }
+
+instance (KnownSymbol symbol, HasElmEncoder Text a, HasElmEndpoints api)
+  => HasElmEndpoints (Servant.QueryParams symbol a :> api) where
+    elmEndpoints' prefix =
+      elmEndpoints' @api prefix
+        { _url = (_url prefix)
+          { _queryString =
+            _queryString (_url prefix) <>
+            [ ( toS $ symbolVal $ Proxy @symbol
+              , List
+              , makeEncoder @Text @a
+              )
+            ]
+          }
+        }
+
+instance (KnownSymbol symbol, HasElmEndpoints api)
+  => HasElmEndpoints (Servant.QueryFlag symbol :> api) where
+    elmEndpoints' prefix =
+      elmEndpoints' @api prefix
+        { _url = (_url prefix)
+          { _queryString =
+            _queryString (_url prefix) <>
+            [ ( toS $ symbolVal $ Proxy @symbol
+              , Flag
+              , Encoder "Basics.identity" "Basics.Bool"
+              )
+            ]
+          }
+        }
+
+instance (HasElmEncoder Aeson.Value a, HasElmEndpoints api, list ~ '[Servant.JSON])
+  => HasElmEndpoints (Servant.ReqBody' mods list a :> api) where
+    elmEndpoints' prefix =
+      elmEndpoints' @api prefix
+        { _body = Just $ makeEncoder @Aeson.Value @a
+        }
+
+instance (KnownSymbol path, HasElmEndpoints api) => HasElmEndpoints (path :> api) where
+  elmEndpoints' prefix =
+    elmEndpoints' @api prefix
+      { _url = (_url prefix)
+        { _path = _path (_url prefix) <> [Static path]
+        }
+      , _functionName = _functionName prefix <> [path]
+      }
+    where
+      path =
+        toS $ symbolVal $ Proxy @path
+
+instance HasElmEndpoints api => HasElmEndpoints (Servant.RemoteHost :> api) where
+  elmEndpoints' = elmEndpoints' @api
+
+instance HasElmEndpoints api => HasElmEndpoints (Servant.IsSecure :> api) where
+  elmEndpoints' = elmEndpoints' @api
+
+instance HasElmEndpoints api => HasElmEndpoints (Servant.Vault :> api) where
+  elmEndpoints' = elmEndpoints' @api
+
+instance HasElmEndpoints api => HasElmEndpoints (Servant.WithNamedContext name context api) where
+  elmEndpoints' = elmEndpoints' @api
+
+instance HasElmEndpoints api => HasElmEndpoints (Servant.HttpVersion :> api) where
+  elmEndpoints' = elmEndpoints' @api
+
+instance HasElmEndpoints api => HasElmEndpoints (Servant.Summary summary :> api) where
+  elmEndpoints' = elmEndpoints' @api
+
+instance HasElmEndpoints api => HasElmEndpoints (Servant.Description description :> api) where
+  elmEndpoints' = elmEndpoints' @api
+
+-------------------------------------------------------------------------------
+-- Orphans
+
+instance HasElmDefinition Servant.NoContent where
+  elmDefinition =
+    Definition.Type "NoContent.NoContent" [("NoContent", [])]
+
+instance HasElmType Servant.NoContent where
+  elmType =
+    "NoContent.NoContent"
+
+instance HasElmDecoder Aeson.Value Servant.NoContent where
+  elmDecoder =
+    Expression.App "Json.Decode.succeed" "NoContent.NoContent"
diff --git a/test/Spec.hs b/test/Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec.hs
@@ -0,0 +1,2 @@
+main :: IO ()
+main = putStrLn "Test suite not yet implemented"
