Skip to content

Commit eb2de5a

Browse files
authored
Merge pull request #1470 from haskell-servant/maksbotan/cookbook-openapi3
Add Cookbook for OpenAPI 3.0
2 parents 907245a + 33210e6 commit eb2de5a

File tree

5 files changed

+227
-0
lines changed

5 files changed

+227
-0
lines changed

cabal.project

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ packages:
5050
doc/cookbook/using-free-client
5151
-- doc/cookbook/open-id-connect
5252
doc/cookbook/managed-resource
53+
doc/cookbook/openapi3
5354

5455
tests: True
5556
optimization: False

doc/cookbook/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ you name it!
1919

2020
structuring-apis/StructuringApis.lhs
2121
generic/Generic.lhs
22+
openapi3/OpenAPI.lhs
2223
https/Https.lhs
2324
db-mysql-basics/MysqlBasics.lhs
2425
db-sqlite-simple/DBConnection.lhs

doc/cookbook/openapi3/OpenAPI.lhs

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
# OpenAPI
2+
3+
OpenAPI is language-agnostic format for API specifications. It is structured as JSON or YAML
4+
document and can be used to communicate API documentation between the backend and its clients, like
5+
the frontend.
6+
7+
The OpenAPI specification itself is available at https://swagger.io/specification/. It is supported
8+
by various tools, like [swagger-ui](https://swagger.io/tools/swagger-ui/) — a tool that
9+
visualizes OpenAPI document and allows to send requests to the backend it describes, or
10+
[swagger-codegen](https://swagger.io/tools/swagger-codegen/), which can generate client code in a
11+
variety of languages given the specification.
12+
13+
Since Servant backends already contain a comprehensive description of the API they provide, it is
14+
fairly easy to generate OpenAPI specification based on that description. This is achieved with
15+
[servant-openapi3](https://hackage.haskell.org/package/servant-openapi3) package, which is based on
16+
older `servant-swagger`, targeted at second version of OpenAPI specification (then called Swagger).
17+
18+
This cookbook demonstrates how to use `servant-openapi3` and how to integrate interactive schema
19+
browser with your backend.
20+
21+
## The sample API
22+
23+
Let's start with an API of an example TODO service:
24+
25+
```haskell
26+
{-# LANGUAGE DataKinds #-}
27+
{-# LANGUAGE TypeOperators #-}
28+
{-# LANGUAGE DeriveGeneric #-}
29+
{-# LANGUAGE DeriveAnyClass #-}
30+
{-# LANGUAGE DerivingStrategies #-}
31+
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
32+
33+
import GHC.Generics
34+
import Data.Text
35+
import Data.Aeson
36+
37+
import Servant
38+
39+
import Data.OpenApi
40+
import Servant.OpenApi
41+
import Servant.Swagger.UI
42+
43+
import Network.Wai.Handler.Warp as Warp
44+
45+
-- | A single Todo entry.
46+
data Todo = Todo
47+
{ created :: Int -- ^ Creation datetime.
48+
, summary :: Text -- ^ Task summary.
49+
}
50+
deriving stock (Show, Generic)
51+
deriving anyclass (ToSchema, ToJSON, FromJSON)
52+
53+
-- | A unique Todo entry ID.
54+
newtype TodoId = TodoId Int
55+
deriving stock (Show, Generic)
56+
deriving newtype (ToJSON, FromHttpApiData)
57+
deriving anyclass (ToParamSchema, ToSchema)
58+
59+
-- | The API of a Todo service.
60+
type TodoAPI
61+
= "todo" :> Description "Get all TODO items"
62+
:> Get '[JSON] [Todo]
63+
:<|> "todo" :> Description "Add a new TODO item"
64+
:> ReqBody '[JSON] Todo :> Post '[JSON] TodoId
65+
:<|> "todo" :> Description "Get a TODO item by its id"
66+
:> Capture "id" TodoId :> Get '[JSON] Todo
67+
:<|> "todo" :> Description "Update an existing TODO item by its id"
68+
:> Capture "id" TodoId :> ReqBody '[JSON] Todo :> Put '[JSON] TodoId
69+
```
70+
71+
Notice that all API endpoints are decorated with `Description` (coming from `servant` itself): these
72+
descriptions will propagate to the OpenAPI document automatically.
73+
74+
## Adding OpenAPI
75+
76+
We are ready to define OpenAPI document for our `TodoAPI`. Everything you need to do for that is to
77+
use `toOpenApi` function from `servant-openapi3` package:
78+
79+
```haskell
80+
-- | OpenAPI spec for Todo API.
81+
todoOpenApi :: OpenApi
82+
todoOpenApi = toOpenApi (Proxy :: Proxy TodoAPI)
83+
```
84+
85+
This is possible since we've derived `ToSchema` for `Todo` and `ToParamSchema` for `TodoId` (needed
86+
since the type is used in URLs) instances &mdash; and this is everything that is needed to generate
87+
the OpenAPI 3.0 specification for our API. All of this is thanks to `Generic`-based schema generator
88+
found in `openapi3` and `servant-openapi3` packages.
89+
90+
Of course, you can customize the schema in many ways, see the documentation for
91+
[`openapi3`](https://hackage.haskell.org/package/openapi3) package.
92+
93+
The generated schema looks something like this:
94+
95+
```json
96+
{
97+
"openapi": "3.0.0",
98+
"info": {
99+
"title": "",
100+
"version": ""
101+
},
102+
"paths": {
103+
"/todo": {
104+
"get": {
105+
"description": "Get all TODO items",
106+
"responses": {
107+
"200": {
108+
"content": {
109+
"application/json;charset=utf-8": {
110+
"schema": {
111+
"items": {
112+
"$ref": "#/components/schemas/Todo"
113+
},
114+
"type": "array"
115+
}
116+
}
117+
},
118+
"description": ""
119+
}
120+
}
121+
}
122+
},
123+
........
124+
}
125+
"components": {
126+
"schemas": {
127+
"Todo": {
128+
"required": [
129+
"created",
130+
"summary"
131+
],
132+
"properties": {
133+
"summary": {
134+
"type": "string"
135+
},
136+
"created": {
137+
"minimum": -9223372036854775808,
138+
"type": "integer",
139+
"maximum": 9223372036854775807
140+
}
141+
},
142+
"type": "object"
143+
},
144+
"TodoId": {
145+
"minimum": -9223372036854775808,
146+
"type": "integer",
147+
"maximum": 9223372036854775807
148+
}
149+
}
150+
}
151+
}
152+
```
153+
154+
The schema can be pasted into the [Swagger editor](https://editor.swagger.io/), which will nicely
155+
display the generated schema.
156+
157+
## Integrating schema browser into the backend
158+
159+
Or, the schema browser can be integrated into the backend itself. This is done via
160+
`servant-swagger-ui` package, which embeds `swagger-ui` into the Servant backend.
161+
162+
First, define a sub-api that will serve the documentation:
163+
164+
```haskell
165+
type DocsAPI = SwaggerSchemaUI "swagger-ui" "swagger.json"
166+
```
167+
168+
And a full API for your backend, which combines your endpoints and `DocsAPI`:
169+
170+
```haskell
171+
type API = DocsAPI :<|> TodoAPI
172+
```
173+
174+
`SwaggerSchemaUI` describes an API that will serve the interactive schema browser at `/swagger-ui`
175+
of your server and the specification in JSON format at `/swagger.json`. Of course, both paths are
176+
customizable.
177+
178+
A handler for `SwaggerSchemaUI`, called `swaggerSchemaUIServer`, expectes one argument: the
179+
specification itself. In our case, it's `todoOpenApi`.
180+
181+
```haskell
182+
todoServer :: Servant.Server API
183+
todoServer = swaggerSchemaUIServer todoOpenApi
184+
:<|> error "The actual TODO API is not implemented"
185+
```
186+
187+
Now the server can be run as usual:
188+
189+
```haskell
190+
main :: IO ()
191+
main = do
192+
Warp.run 5000 $ serve (Proxy :: Proxy API) todoServer
193+
```
194+
195+
Run this example, navigate to http://localhost:5000/swagger-ui and you will see the interactive
196+
schema browser:
197+
198+
![](./swagger-ui-example.png)
199+
200+
You can make requests in this UI and they will be sent to your backend as expected.

doc/cookbook/openapi3/openapi3.cabal

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
name: cookbook-openapi3
2+
version: 2.2
3+
synopsis: OpenAPI 3.0 schema generation example
4+
homepage: http://docs.servant.dev/
5+
license: BSD3
6+
license-file: ../../../servant/LICENSE
7+
author: Servant Contributors
8+
maintainer: [email protected]
9+
build-type: Simple
10+
cabal-version: >=1.10
11+
12+
executable cookbook-openapi3
13+
main-is: OpenAPI.lhs
14+
build-tool-depends: markdown-unlit:markdown-unlit
15+
default-language: Haskell2010
16+
ghc-options: -Wall -pgmL markdown-unlit
17+
build-depends: base >= 4.9 && <5
18+
, aeson
19+
, openapi3
20+
, servant
21+
, servant-server
22+
, servant-openapi3
23+
, servant-swagger-ui
24+
, text
25+
, warp
145 KB
Loading

0 commit comments

Comments
 (0)