Servant is a Haskell ecosystem for describing an HTTP API as a type, then checking a server implementation against that description. One API definition records routes, HTTP methods, request bodies, and response formats, keeping network details behind the library’s abstractions. The same description can also support generated clients and documentation. section 17.1 section 17.0
An API type is assembled with type operators. (:>) extends one endpoint description with a path segment, request requirement, or response; (:<|>) joins endpoint descriptions into one larger API. GET, POST, PATCH, DELETE, ReqBody, Capture, and response combinators express the HTTP-facing contract directly in the type. JSON responses use forms such as Get '[JSON] SomeType, while a request body can require ReqBody '[JSON] SomeType. A body that does not meet the declared specification is rejected automatically. section 17.1.1
Listing 17.1 A type for the todo list API server
{-# LANGUAGE TypeOperators #-} #1
{-# LANGUAGE DataKinds #-} #1
module Todos.Api where
import Servant.API
import Todos.Types
type TodoApi =
"entries" :> Get '[JSON] [TodoEntry] #2
:<|> "add" :> ReqBody '[JSON] TodoEntry :>
PostCreated '[JSON] Int #3
:<|> "toggle" :> Capture "id" Int :> PatchNoContent #4
A todo API combines a JSON listing endpoint, an endpoint that accepts a TodoEntry and returns its identifier, and an identifier-capturing toggle endpoint. section 17.1.1
A server implementation mirrors the API type with handlers. Handler represents the action for one endpoint; Server combines those actions for the complete specification. For example, a database action returning [TodoEntry] can become Handler [TodoEntry] through liftIO, leaving JSON conversion to Servant. Supplying database actions as handler arguments limits direct connection access and makes fake database actions easier to substitute. section 17.1.3
Handlers can translate application results into HTTP responses while keeping their type-shaped inputs and outputs:
- An add handler receives a TodoEntry, runs a supplied insertion action, returns its identifier on success, and turns a SQLite error into an HTTP 422 response with an error body. section 17.1.3
- A toggle handler receives an Int capture, finds and updates an entry, returns NoContent on success, and raises HTTP 404 when no entry exists. section 17.1.3
- The server joins endpoint handlers with (:<|>), in the same order as the TodoApi declaration. section 17.1.3
Servant produces a WAI Application, so routes that should not belong to the machine-facing API can be added through Middleware. Middleware has type Application -> Application: it wraps an application, inspects a request, and either supplies a response or delegates to the wrapped application. This separates a root HTML todo-list page from the JSON API because a derived API client should not need to request that page. section 17.2.2
The servant-client package derives client actions from the same API type. client accepts a Proxy for the API and produces a ClientM action for each endpoint. Endpoint results become ClientM result types, while request bodies and captures become function arguments; JSON encoding is handled automatically. runClientM executes a ClientM action with a ClientEnv and yields either ClientError or a result in IO. section 17.3.1 section 17.4
Listing 17.13 Derivation of client functionality from the API’s Proxy
getEntries :: ClientM [TodoEntry] #1 addEntry :: TodoEntry -> ClientM Int #1 toggleEntry :: Int -> ClientM NoContent #1 getEntries :<|> addEntry :<|> toggleEntry = client todoApi #2
The generated client declaration exposes three actions whose argument and result types follow the entries, add, and toggle API endpoints. section 17.3.1
The practical payoff is a single API contract shared by server and client code. Define the route structure once, implement handlers matching it, use ServerError values for fitting failures, and derive client functions instead of hand-writing HTTP calls. section 17.4

