Servant 0.20.3.0 and Scrive

The Servant web framework version 0.20.3.0 has been released, with contributions from Scrive. We are very proud to contribute our time to Servant, which is one of the finest pieces of software for industrial users of Haskell

The Servant web framework version 0.20.3.0 has been released, with contributions from Scrive.
We are very proud to contribute our time to Servant, which is one of the finest pieces of software for industrial users of Haskell.

MultiVerb

Servant v0.20.3.0 brings the new MultiVerb type to better express complex API routes.

The inclusion of MultiVerb in the servant code base is the product of a collaboration between Scrive and Wire.
This was a great opportunity for two actors of different industry to combine their strength and make battle-tested solutions available to the public.
This is hopefully only the beginning of more partnerships with Wire, whose open-source code base demonstrates the power of Haskell in production.

Opaque route types

The problem with the traditional way of declaring a route, using the type Verb, is that we can only represent one status code and one HTTP method:

data Verb (method :: k1)
          (statusCode :: Nat)
          (contentTypes :: [Type])
          (a :: Type)

Which means that endpoint definitions using Verb can only represent the happy path of an endpoint. Failures must be expressed out-of-band, by throwing unchecked exceptions.

Ironically, this goes against what we are used to when working with Servant: Getting as much useful information as possible from the route types. Moreover, generated documentation – like OpenAPI specifications – from these types will miss the very real endpoint responses.

Union Verb, or UVerb

Servant already has a mechanism called UVerb, with the U standing for Union.
However, this mechanism suffers from several disadvantages, the most important being that there is a fundamental incompatibilty between Verb and UVerb,
which forces users into using servant-specific functions in their handlers.

For instance, let us consider a route for handling sign-ins:

type CreateSession =
  "new"
    :> ReqBody '[FormUrlEncoded] LoginForm
    :> UVerb 'POST '[HTML] CreateSessionResponses

type CreateSessionResponses =
  '[ -- Failure, send login page back
     WithStatus 403 (Html ())
   , -- Success, redirected to home page
     WithStatus 301 (Headers '[Header "Location" Text, Header "Set-Cookie" SetCookie] NoContent)
   ]

This route declaration reads as

In the /new path, we expect a POST request with a body (LoginForm). If the authentication fails we reply with status 403 and HTML. If the authentication is successful we reply with a 301 and redirect to another page while also giving the user a cookie to keep them connected.

And the handler will look something like:

createSessionHandler
  :: LoginForm
  -> m (Union CreateSessionResponses)
createSessionHandler LoginForm{email, password} = do
  mUser <- Query.getUserByEmail email
  case mUser of
    Nothing ->
      respond $ WithStatus @403 $ render Sessions.newSession
    Just user ->
      if Sel.verifyText user.password password
        then do
          sessionId <- persistSession session.sessionId user.userId
          let cookie = craftSessionCookie sessionId True
          respond $ WithStatus @301 $ redirectWithCookie "/" cookie
        else do
          respond $ WithStatus @403 $ render Sessions.newSession

redirectWithCookie
    :: Text
    -> SetCookie
    -> Headers '[Header "Location" Text, Header "Set-Cookie" SetCookie]
               NoContent
redirectWithCookie destination cookie =
  addHeader destination (addHeader cookie NoContent)

UVerb's list of HTTP responses is used both for the type signatures of the route handlers, and as the return values within the handlers.
We see that the WithStatus construct is indeed reflected in the handler and we have to disambiguate the status code as a Type Application, which is rather redundant and prone to error.
A function like redirectWithCookie is also needed in order to bring HTTP response concerns in the code of the handler, which is quite invasive.

Better ergonomics with MultiVerb

In contrast, here is the equivalent route declaration with MultiVerb:

type CreateSessionResponses =
  '[ -- Failure, send login page back
     Respond 403 "Authentication failed" (Html ())
   , -- Success, redirected to home page
     WithHeaders
       '[Header "Location" Text, Header "Set-Cookie" SetCookie]
       (Text, SetCookie)
       (RespondEmpty 301 "Authentication succeeded")
   ]

We have here a type-level list of HTTP responses, with details about the status code and the headers.
These details do not leak into the handler, because we define the values that the handler will return:

data CreateSessionResult
  = AuthenticationFailure (Html ())
  | AuthenticationSuccess (Text, SetCookie)
  deriving stock (Generic)
  deriving
    (AsUnion CreateSessionResponses)
    via GenericAsUnion CreateSessionResponses CreateSessionResult

This is a simple sum type, which we bind to the route responses to ensure consistency. Each member of this union holds a return type that will get serialised on the network: No need for status codes.

Finally, we declare the endpoint as such:

type CreateSession =
  "new"
    :> ReqBody '[FormUrlEncoded] LoginForm
    :> MultiVerb
         'POST
         '[HTML]
         CreateSessionResponses
         CreateSessionResult

And the handler will look as such:

createSessionHandler
  :: LoginForm
  -> m CreateSessionResult
createSessionHandler LoginForm{email, password} = do
  mUser <- Query.getUserByEmail email
  case mUser of
    Nothing -> do
      body <- render Sessions.newSession
      pure $ AuthenticationFailure body
    Just user ->
      if Sel.verifyText user.password password
        then do
          sessionId <- persistSession session.sessionId user.userId
          let sessionCookie = craftSessionCookie sessionId True
          pure $ AuthenticationSuccess ("/", sessionCookie)
        else do
          body <- render templateEnv Sessions.newSession
          pure $ AuthenticationFailure body

We are now using good old pure without having to use type applications, which makes the code much more readable and lets us focus on the important things

This is a good step forward for more expressive and ergonomic endpoint types within Servant, which has always been at the forefront of innovation when
it comes to using type-level programming to bring safety and generated documentation to web development.

Servant.API.Range - Bounded Type-Level Values

Another powerful feature in Servant 0.20.3.0 is the extended use of the Range type, which can enforce compile-time bounds on numeric values.

Strongly-Typed Bounded Values

Flavio Corpa worked on the Range type, that allows you to define values that must fall within specific bounds at the type level:

type MaxAttempts = Range 1 5

This declares a type that can only contain integers between 1 and 5 (inclusive). If you try to construct a value outside these bounds, you'll get a compile-time error.

Usage in API Definitions

This is particularly useful for API parameters that have logical constraints:

type API =
  "limited-query"
    :> QueryParam "attempts" MaxAttempts
    :> Get '[JSON] Response

By using Range, we ensure that handlers only ever receive valid values, eliminating an entire class of runtime checks and potential errors.

Implementation Example

Here's how you might use this in a handler:

handler :: Maybe MaxAttempts -> Handler Response
handler Nothing = -- Default case, perhaps using 3 attempts
  performOperation (fromJust $ mkRange @1 @5 3)
handler (Just attempts) = -- We know attempts is between 1-5
  performOperation attempts

performOperation :: MaxAttempts -> Handler Response
performOperation attempts = do
  -- We can safely use the value knowing it's within bounds
  let attemptCount = unRange attempts
  -- ... rest of implementation

The mkRange smart constructor returns a Maybe that's only Nothing if the value is out of bounds.

Benefits of Type-Level Constraints

Using Range for bounded values provides several advantages:

  1. Compile-time safety: Invalid values are caught early in the development cycle
  2. Self-documenting code: The constraints are visible in the type signature
  3. Elimination of validation boilerplate: No need for runtime checks in handler code
  4. Clear API contracts: Users of your API know exactly what values are acceptable

Combined with the other features like MultiVerb, Servant continues to leverage Haskell's type system to provide safe, expressive, and robust APIs.