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] Bookmeans:GET /books/:id, whereidmust parse as anInt, returning aBookas JSON. A request with/books/abcgets a 400 error automatically.:<|>separates endpoints, and the server is the handlers joined with the same:<|>, in the same order.- Handlers run in the
Handlermonad:throwError err404returns an HTTP error, andliftIOruns 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, orpersistent - Configuration — environment variables via
System.Environmentorenvparse - Logging —
katip,co-log, orfast-logger - Clients —
servant-clientgenerates 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.