reitit/doc/ring/swagger.md
2025-10-24 15:52:47 +03:00

305 lines
12 KiB
Markdown

# Swagger Support
```
[metosin/reitit-swagger "0.9.2-rc1"]
```
Reitit supports [Swagger2](https://swagger.io/) documentation, thanks to [schema-tools](https://github.com/metosin/schema-tools) and [spec-tools](https://github.com/metosin/spec-tools). Documentation is extracted from route definitions, coercion `:parameters` and `:responses` and from a set of new documentation keys.
See also: [OpenAPI support](openapi.md).
To enable swagger-documentation for a Ring router:
1. annotate your routes with swagger-data
2. mount a swagger-handler to serve the swagger-spec
3. optionally mount a swagger-ui to visualize the swagger-spec
## Swagger data
The following route data keys contribute to the generated swagger specification:
| key | description |
| --------------|-------------|
| :swagger | map of any swagger-data. Can have `:id` (keyword or sequence of keywords) to identify the api
| :no-doc | optional boolean to exclude endpoint from api docs
| :tags | optional set of string or keyword tags for an endpoint api docs
| :summary | optional short string summary of an endpoint
| :description | optional long description of an endpoint. Supports http://spec.commonmark.org/
| :operationId | optional string specifying the unique ID of an Operation
Coercion keys also contribute to the docs:
| key | description |
| --------------|-------------|
| :parameters | optional input parameters for a route, in a format defined by the coercion
| :responses | optional descriptions of responses, in a format defined by coercion
There is a `reitit.swagger.swagger-feature`, which acts as both a `Middleware` and an `Interceptor` that is not participating in any request processing - it just defines the route data specs for the routes it's mounted to. It is only needed if the [route data validation](route_data_validation.md) is turned on.
## Swagger spec
To serve the actual [Swagger Specification](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md), there is `reitit.swagger/create-swagger-handler`. It takes no arguments and returns a ring-handler which collects at request-time data from all routes for the same swagger api and returns a formatted Swagger specification as Clojure data, to be encoded by a response formatter.
If you need to post-process the generated spec, just wrap the handler with a custom `Middleware` or an `Interceptor`.
## Swagger-ui
[Swagger-ui](https://github.com/swagger-api/swagger-ui) is a user interface to visualize and interact with the Swagger specification. To make things easy, there is a pre-integrated version of the swagger-ui as a separate module.
```
[metosin/reitit-swagger-ui "0.9.2-rc1"]
```
`reitit.swagger-ui/create-swagger-ui-handler` can be used to create a ring-handler to serve the swagger-ui. It accepts the following options:
| key | description |
| -----------------|-------------|
| :parameter | optional name of the wildcard parameter, defaults to unnamed keyword `:`
| :root | optional resource root, defaults to `"swagger-ui"`
| :url | path to swagger endpoint, defaults to `/swagger.json`
| :path | optional path to mount the handler to. Works only if mounted outside of a router.
| :config | parameters passed to swagger-ui as-is. See [the docs](https://github.com/swagger-api/swagger-ui/tree/2.x#parameters)
We use swagger-ui from [ring-swagger-ui](https://github.com/metosin/ring-swagger-ui), which can be easily configured from routing application. It stores files `swagger-ui` in the resource classpath.
Webjars also hosts a [version](https://github.com/webjars/swagger-ui) of the swagger-ui.
**NOTE**: Currently, swagger-ui module is just for Clojure. ClojureScript-support welcome as a PR!
**NOTE:** If you want to use swagger-ui 2.x you can do so by explicitly downgrading `metosin/ring-swagger-ui` to `2.2.10`.
**NOTE:** If you use swagger-ui 3.x, you need to include `:responses` for Swagger-UI
to display the response when trying out endpoints. You can define `:responses {200 {:schema s/Any}}`
at the top-level to show responses for all endpoints.
## Examples
### Simple example
* two routes
* swagger-spec served from `"/swagger.json"`
* swagger-ui mounted to `"/api-docs"`
* note that for real-world use, you need a [content-negotiation middleware][muuntaja] -
see the next example
[muuntaja]: ../ring/default_middleware.md#content-negotiation
```clj
(require '[reitit.ring :as ring])
(require '[reitit.swagger :as swagger])
(require '[reitit.swagger-ui :as swagger-ui])
(def app
(ring/ring-handler
(ring/router
[["/api"
["/ping" {:get (constantly {:status 200, :body "ping"})}]
["/pong" {:post (constantly {:status 200, :body "pong"})}]]
["" {:no-doc true}
["/swagger.json" {:get (swagger/create-swagger-handler)}]
["/api-docs/*" {:get (swagger-ui/create-swagger-ui-handler)}]]])))
```
The generated swagger spec:
```clj
(app {:request-method :get :uri "/swagger.json"})
;{:status 200
; :body {:swagger "2.0"
; :x-id #{:reitit.swagger/default}
; :paths {"/api/ping" {:get {}}
; "/api/pong" {:post {}}}}}
```
Swagger-ui:
```clj
(app {:request-method :get, :uri "/api-docs/index.html"})
; ... the swagger-ui index-page, configured correctly
```
You might be interested in adding a [trailing slash handler](slash_handler.md) to the app to serve the swagger-ui from `/api-docs` (without the trailing slash) too.
Another way to serve the swagger-ui is using the [default handler](default_handler.md):
```clj
(def app
(ring/ring-handler
(ring/router
[["/api"
["/ping" {:get (constantly {:status 200, :body "ping"})}]
["/pong" {:post (constantly {:status 200, :body "pong"})}]]
["/swagger.json"
{:get {:no-doc true
:handler (swagger/create-swagger-handler)}}]])
(swagger-ui/create-swagger-ui-handler {:path "/api-docs"})))
```
### More complete example
* `clojure.spec` coercion
* swagger data (`:tags`, `:produces`, `:summary`, `:basePath`)
* swagger-spec served from `"/swagger.json"`
* swagger-ui mounted to `"/"`
* set of middleware for content negotiation, exceptions, multipart etc.
* missed routes are handled by `create-default-handler`
* served via [ring-jetty](https://github.com/ring-clojure/ring/tree/master/ring-jetty-adapter)
Whole example project is in [`/examples/ring-spec-swagger`](https://github.com/metosin/reitit/tree/master/examples/ring-spec-swagger).
```clj
(ns example.server
(:require [reitit.ring :as ring]
[reitit.swagger :as swagger]
[reitit.swagger-ui :as swagger-ui]
[reitit.ring.coercion :as coercion]
[reitit.coercion.spec]
[reitit.ring.middleware.muuntaja :as muuntaja]
[reitit.ring.middleware.exception :as exception]
[reitit.ring.middleware.multipart :as multipart]
[reitit.ring.middleware.parameters :as parameters]
[ring.middleware.params :as params]
[ring.adapter.jetty :as jetty]
[muuntaja.core :as m]
[clojure.java.io :as io]))
(def app
(ring/ring-handler
(ring/router
[["/swagger.json"
{:get {:no-doc true
:swagger {:info {:title "my-api"}
:basePath "/"} ;; prefix for all paths
:handler (swagger/create-swagger-handler)}}]
["/files"
{:swagger {:tags ["files"]}}
["/upload"
{:post {:summary "upload a file"
:parameters {:multipart {:file multipart/temp-file-part}}
:responses {200 {:body {:file multipart/temp-file-part}}}
:handler (fn [{{{:keys [file]} :multipart} :parameters}]
{:status 200
:body {:file file}})}}]
["/download"
{:get {:summary "downloads a file"
:swagger {:produces ["image/png"]}
:handler (fn [_]
{:status 200
:headers {"Content-Type" "image/png"}
:body (io/input-stream (io/resource "reitit.png"))})}}]]
["/math"
{:swagger {:tags ["math"]}}
["/plus"
{:get {:summary "plus with spec query parameters"
:parameters {:query {:x int?, :y int?}}
:responses {200 {:body {:total int?}}}
:handler (fn [{{{:keys [x y]} :query} :parameters}]
{:status 200
:body {:total (+ x y)}})}
:post {:summary "plus with spec body parameters"
:parameters {:body {:x int?, :y int?}}
:responses {200 {:body {:total int?}}}
:handler (fn [{{{:keys [x y]} :body} :parameters}]
{:status 200
:body {:total (+ x y)}})}}]]]
{:data {:coercion reitit.coercion.spec/coercion
:muuntaja m/instance
:middleware [;; query-params & form-params
parameters/parameters-middleware
;; content-negotiation
muuntaja/format-negotiate-middleware
;; encoding response body
muuntaja/format-response-middleware
;; exception handling
exception/exception-middleware
;; decoding request body
muuntaja/format-request-middleware
;; coercing response bodys
coercion/coerce-response-middleware
;; coercing request parameters
coercion/coerce-request-middleware
;; multipart
multipart/multipart-middleware]}})
(ring/routes
(swagger-ui/create-swagger-ui-handler {:path "/"})
(ring/create-default-handler))))
(defn start []
(jetty/run-jetty #'app {:port 3000, :join? false})
(println "server running in port 3000"))
```
http://localhost:3000 should render now the swagger-ui:
![Swagger-ui](../images/swagger.png)
## Multiple swagger apis
There can be multiple swagger apis within a router. Each route can be part of 0..n swagger apis. Swagger apis are identified by value in route data under key path `[:swagger :id]`. It can be either a keyword or a sequence of keywords. Normal route data [scoping rules](../basics/route_data.md#nested-route-data) rules apply.
Example with:
* 4 routes
* 2 swagger apis `::one` and `::two`
* 3 swagger specs
```clj
(require '[reitit.ring :as ring])
(require '[reitit.swagger :as swagger])
(def ping-route
["/ping" {:get (constantly {:status 200, :body "ping"})}])
(def spec-route
["/swagger.json"
{:get {:no-doc true
:handler (swagger/create-swagger-handler)}}])
(def app
(ring/ring-handler
(ring/router
[["/common" {:swagger {:id #{::one ::two}}} ping-route]
["/one" {:swagger {:id ::one}} ping-route spec-route]
["/two" {:swagger {:id ::two}} ping-route spec-route
["/deep" {:swagger {:id ::one}} ping-route]]
["/one-two" {:swagger {:id #{::one ::two}}} spec-route]])))
```
```clj
(-> {:request-method :get, :uri "/one/swagger.json"} app :body :paths keys)
; ("/common/ping" "/one/ping" "/two/deep/ping")
```
```clj
(-> {:request-method :get, :uri "/two/swagger.json"} app :body :paths keys)
; ("/common/ping" "/two/ping")
```
```clj
(-> {:request-method :get, :uri "/one-two/swagger.json"} app :body :paths keys)
; ("/common/ping" "/one/ping" "/two/ping" "/two/deep/ping")
```
## Reusable schema definitions
Swagger supports having reusable schema definitions under the
`"definitions"` key. These can be reused in different parts of
swagger.json using the `"$ref": "#/definitions/Foo"` syntax. These
definitions are also rendered in their own section in Swagger UI.
Reusable schema objects are generated for Malli `:ref`s and vars.
Currently (as of 0.7.2), reusable schema objects are **not** generated
for Plumatic Schema or Spec.
## TODO
* ClojureScript
* example for [Macchiato](https://github.com/macchiato-framework)
* body formatting
* resource handling