OpenAPI

OpenAPI is language-agnostic format for API specifications. It is structured as JSON or YAML document and can be used to communicate API documentation between the backend and its clients, like the frontend.

The OpenAPI specification itself is available at https://swagger.io/specification/. It is supported by various tools, like swagger-ui — a tool that visualizes OpenAPI document and allows to send requests to the backend it describes, or swagger-codegen, which can generate client code in a variety of languages given the specification.

Since Servant backends already contain a comprehensive description of the API they provide, it is fairly easy to generate OpenAPI specification based on that description. This is achieved with servant-openapi3 package, which is based on older servant-swagger, targeted at second version of OpenAPI specification (then called Swagger).

This cookbook demonstrates how to use servant-openapi3 and how to integrate interactive schema browser with your backend.

The sample API

Let’s start with an API of an example TODO service:

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}

import GHC.Generics
import Data.Text
import Data.Aeson

import Servant

import Data.OpenApi
import Servant.OpenApi
import Servant.Swagger.UI

import Network.Wai.Handler.Warp as Warp

-- | A single Todo entry.
data Todo = Todo
  { created :: Int  -- ^ Creation datetime.
  , summary :: Text -- ^ Task summary.
  }
  deriving stock (Show, Generic)
  deriving anyclass (ToSchema, ToJSON, FromJSON)

-- | A unique Todo entry ID.
newtype TodoId = TodoId Int
  deriving stock (Show, Generic)
  deriving newtype (ToJSON, FromHttpApiData)
  deriving anyclass (ToParamSchema, ToSchema)

-- | The API of a Todo service.
type TodoAPI
    = "todo" :> Description "Get all TODO items"
             :> Get '[JSON] [Todo]
 :<|> "todo" :> Description "Add a new TODO item"
             :> ReqBody '[JSON] Todo :> Post '[JSON] TodoId
 :<|> "todo" :> Description "Get a TODO item by its id"
             :> Capture "id" TodoId :> Get '[JSON] Todo
 :<|> "todo" :> Description "Update an existing TODO item by its id"
             :> Capture "id" TodoId :> ReqBody '[JSON] Todo :> Put '[JSON] TodoId

Notice that all API endpoints are decorated with Description (coming from servant itself): these descriptions will propagate to the OpenAPI document automatically.

Adding OpenAPI

We are ready to define OpenAPI document for our TodoAPI. Everything you need to do for that is to use toOpenApi function from servant-openapi3 package:

-- | OpenAPI spec for Todo API.
todoOpenApi :: OpenApi
todoOpenApi = toOpenApi (Proxy :: Proxy TodoAPI)

This is possible since we’ve derived ToSchema for Todo and ToParamSchema for TodoId (needed since the type is used in URLs) instances — and this is everything that is needed to generate the OpenAPI 3.0 specification for our API. All of this is thanks to Generic-based schema generator found in openapi3 and servant-openapi3 packages.

Of course, you can customize the schema in many ways, see the documentation for openapi3 package.

The generated schema looks something like this:

{
  "openapi": "3.0.0",
  "info": {
    "title": "",
    "version": ""
  },
  "paths": {
    "/todo": {
      "get": {
        "description": "Get all TODO items",
        "responses": {
          "200": {
            "content": {
              "application/json;charset=utf-8": {
                "schema": {
                  "items": {
                    "$ref": "#/components/schemas/Todo"
                  },
                  "type": "array"
                }
              }
            },
            "description": ""
          }
        }
      }
    },
    ........
  }
    "components": {
    "schemas": {
      "Todo": {
        "required": [
          "created",
          "summary"
        ],
        "properties": {
          "summary": {
            "type": "string"
          },
          "created": {
            "minimum": -9223372036854775808,
            "type": "integer",
            "maximum": 9223372036854775807
          }
        },
        "type": "object"
      },
      "TodoId": {
        "minimum": -9223372036854775808,
        "type": "integer",
        "maximum": 9223372036854775807
      }
    }
  }
}

The schema can be pasted into the Swagger editor, which will nicely display the generated schema.

Integrating schema browser into the backend

Or, the schema browser can be integrated into the backend itself. This is done via servant-swagger-ui package, which embeds swagger-ui into the Servant backend.

First, define a sub-api that will serve the documentation:

type DocsAPI = SwaggerSchemaUI "swagger-ui" "swagger.json"

And a full API for your backend, which combines your endpoints and DocsAPI:

type API = DocsAPI :<|> TodoAPI

SwaggerSchemaUI describes an API that will serve the interactive schema browser at /swagger-ui of your server and the specification in JSON format at /swagger.json. Of course, both paths are customizable.

A handler for SwaggerSchemaUI, called swaggerSchemaUIServer, expectes one argument: the specification itself. In our case, it’s todoOpenApi.

todoServer :: Servant.Server API
todoServer = swaggerSchemaUIServer todoOpenApi
  :<|> error "The actual TODO API is not implemented"

Now the server can be run as usual:

main :: IO ()
main = do
  Warp.run 5000 $ serve (Proxy :: Proxy API) todoServer

Run this example, navigate to http://localhost:5000/swagger-ui and you will see the interactive schema browser:

You can make requests in this UI and they will be sent to your backend as expected.