Skip to content
AgileLogics
8 min read

Web APIs with Servant

Prerequisites: JSON with aeson

Servant describes a web API as a type. From that one type it derives the server's routing, request parsing, and response encoding — and can also generate client functions and OpenAPI documentation. If your handlers don't match the API type, the program doesn't compile.

Other popular web frameworks are Scotty (small, Sinatra-like), Yesod (full-stack), and IHP (batteries-included, with its own tooling). All of them run on the Warp web server.

An API type

-- illustrative: needs the servant-server, warp, and aeson packages
{-# LANGUAGE DataKinds, TypeOperators, DeriveGeneric, DeriveAnyClass #-}

import Data.Aeson (FromJSON, ToJSON)
import GHC.Generics (Generic)
import Network.Wai.Handler.Warp (run)
import Servant

data Book = Book {bookId :: Int, title :: String}
  deriving (Show, Generic, ToJSON, FromJSON)

type BookAPI =
  "books" :> Get '[JSON] [Book]
    :<|> "books" :> Capture "id" Int :> Get '[JSON] Book
    :<|> "books" :> ReqBody '[JSON] Book :> Post '[JSON] Book

books :: [Book]
books = [Book 1 "Learn You a Haskell", Book 2 "Real World Haskell"]

server :: Server BookAPI
server = listBooks :<|> getBook :<|> createBook
  where
    listBooks = return books
    getBook i = case filter ((== i) . bookId) books of
      (b : _) -> return b
      [] -> throwError err404
    createBook b = return b

main :: IO ()
main = do
  putStrLn "Listening on http://localhost:8080"
  run 8080 (serve (Proxy :: Proxy BookAPI) server)

Try it from another terminal:

$ curl http://localhost:8080/books/1
{"bookId":1,"title":"Learn You a Haskell"}
$ curl -i http://localhost:8080/books/9
HTTP/1.1 404 Not Found
...
  • "books" :> Capture "id" Int :> Get '[JSON] Book means: GET /books/:id, where id must parse as an Int, returning a Book as JSON. A request with /books/abc gets a 400 error automatically.
  • :<|> separates endpoints, and the server is the handlers joined with the same :<|>, in the same order.
  • Handlers run in the Handler monad: throwError err404 returns an HTTP error, and liftIO runs IO actions.

Real applications

A real service keeps its state (a database pool, a TVar, a logger) in an environment passed to the handlers — often with hoistServer to run handlers in your own ReaderT Env IO monad. Typical companions:

  • Database — postgresql-simple, hasql, or persistent
  • Configuration — environment variables via System.Environment or envparse
  • Logging — katip, co-log, or fast-logger
  • Clients — servant-client generates typed client functions from the same API type, perfect for integration tests

Security basics

  • Decode request bodies into precise types (as above) — invalid input is rejected before your handler runs.
  • Limit request body sizes and add timeouts in Warp's settings for public services.
  • Use parameterized queries in database libraries (all of the above do this by default) — never build SQL by string concatenation.
  • Keep secrets in environment variables, not in the source code.