Skip to content

Building Documented Apis

Will edited this page Jul 17, 2018 · 26 revisions

Sweet

Namespace compojure.api.sweet is a public entry point for most of the routing stuff. It imports the enhanced route macros from compojure.api.core, swagger-routes from compojure.api.swagger, apis from compojure.api.api and a few extras from ring.swagger.json-schema. In most cases, it's the only import you need.

The following examples expect the following imports

(require '[compojure.api.sweet :refer :all])
(require '[ring.util.http-response :refer :all])

Api middleware

compojure.api.middleware/api-middleware wraps the basic functionality for an web api. It's a enhanced version of compojure.handler/api adding the following:

  • catching slingshotted http-errors (ring.middleware.http-response/catch-response)
  • catching unhandled exceptions (compojure.api.middleware/wrap-exceptions)
  • support for different protocols via ring.middleware.format-params/wrap-restful-params and ring.middleware.format-response/wrap-restful-response
    • default supported protocols are: :json-kw, :yaml-kw, :edn, :transit-json and :transit-msgpack
  • enabled protocol support is also published into Swagger docs via ring.swagger.middleware/wrap-swagger-data

All middlewares are preconfigured with good/opinionated defaults, but one can override the configurations by passing an options Map into the api-middleware. See api-docs for details.

Api

The top-level route-function in compojure-api is compojure.api.api/api. It takes an optional options map and a sequence of ring-handlers for request processing. Api mounts the api-middleware and creates a route-tree from the handlers and injects it to the to be used in swagger-docs and bi-directional routing. Optionally, one can configure all the swagger artefacts also via api options under key :swagger. See api-docs for details.

Api with defaults

(def app
  (api
    (GET "/ping" []
      (ok {:ping "pong"}))))

(slurp (:body (app {:request-method :get :uri "/ping"})))
; => "{\"ping\":\"pong\"}"

Api with custom options

(def app
  (api
    {:api {:invalid-routes-fn handle-missing-routes-fn}
     :exceptions {:handlers {::ex/default custom-exception-handler
                             ::custom-error custom-error-handler}}
     :format {:formats [:json-kw :edn :transit-json]
              :response-opts {:transit-json {:handlers transit/writers}}
              :params-opts {:transit-json {:handlers transit/readers}}}
     :coercion my-domain-coercion
     :ring-swagger {:ignore-missing-mappings? true}
     :swagger {:ui "/"
               :spec "/swagger.json"
               :options {:ui {:validatorUrl nil}
               :data {:info {:version "1.0.0", :title "Thingies API"}}
                      :tags [{:name "math", :description "Math with parameters"}]}}}
    (GET "/ping" []
      (ok {:ping "pong"}))))

Defapi

compojure.api.core/defapi(deprecated 2.0.0-alpha17) is just a shortcut for defining an api:

(defapi app
  (context "/api" []
    (GET "/ping" []
      (ok {:ping "pong"}))))

Middleware

compojure.api.core/middleware is a utility macro for mounting Middleware on the path. It takes a vector of middleware, presented in any of these forms:

  • a function value, e.g. wrap-foo
  • an anonymous function #(wrap-params % {:keywords true})
  • a vector of function and its non-handler arguments [wrap-params {:keywords true}]

Similar to Duct.

(defapi app
  (middleware [wrap-head [wrap-params {:keywords true}]]
    (context "/api" []
      (GET "/ping" []
        (ok {:ping "pong"})))))

There is also a restucturing for middleware, the :middleware. Same rules apply.

(defapi app
  (context "/api" []
    :middleware [wrap-foo]
    (GET "/ping" []
      :middleware [wrap-bar]
      (ok {:ping "pong"}))))

Endpoint macros

Compojure-api wraps the vanilla Compojure route macros in compojure.api.core. They can be used just like the orgininals, but also define a new way of extending their behavior via restructuring.

  • GET, POST, PUT, HEAD, PATCH, DELETE,OPTIONS and ANY.
; just like a compojure route
(GET "/ping" []
  (ok {:ping "pong"}))

; with automatic query-string and response model coercion (with support for generated swagger-docs)
(GET "/plus" []
  :query-params [x :- Long, y :- Long]
  :return {:result Long}
  (ok {:result (+ x y)}))

See more details on Restucturing.

Route documentation

Compojure-api uses Swagger for route documentation.

Sample Swagger app

(defroutes legacy-route
  (GET "/ping/:id" [id]
    (ok {:id id})))

(defapi app
  (swagger-routes)
  (context "/api" []
    legacy-route
    (POST "/echo" []
      :return {:name String}
      :body [body {:name String}]
      (ok body))))

The above sample application mounts swagger-docs to root / and serves the swagger-docs from /swagger.json.

Api Validation

To ensure that your API is valid, one can call compojure.api.validator/validate. It takes the api (the ring handler returned by api or defapi) as an parameter and returns the api or throws an Exception. The validation does the following:

  1. if the api is not a swagger api (does not have the swagger-docs mounted) and compiles, it's valid
  2. if the api is a swagger api (does have the swagger-docs mounted):
    • Ring Swagger is called to verify that all Schemas can be transformed to Swagger JSON Schemas
    • the swagger-spec endpoint is called with 200 response status
    • the generated swagger-spec is in align to the Swagger JSON Schema
(require '[compojure.api.sweet :refer :all])
(require '[compojure.api.validator :refer [validate])

(defrecord NonSwaggerRecord [data])

(def app
  (validate
    (api
      (swagger-routes)
      (GET "/ping" []
        :return NonSwaggerRecord
        (ok (->NonSwaggerRecord "ping"))))))

; clojure.lang.Compiler$CompilerException: java.lang.IllegalArgumentException:
; don't know how to create json-type of: class compojure.api.integration_test.NonSwaggerRecord

Bi-directional routing

Inspired by the awesome bidi, Compojure-api also supports bi-directional routing. Routes can be attached with a :name and other endpoints can refer to them via path-for macro (or path-for* function). path-for takes the route-name and optionally a map of path-parameters needed to construct the full route. Normal ring-swagger path-parameter serialization is used, so one can use all supported Schema elements as the provided parameters.

Route names should be keywords. Compojure-api ensures that there are no duplicate endpoint names within an api, raising a IllegalArgumentException at compile-time if it finds multiple routes with the same name. Route name is published as :x-name into the Swagger docs.

(fact "bi-directional routing with path-parameters"
    (let [app (api
                (GET "/lost-in/:country/:zip" []
                  :name :lost
                  :path-params [country :- (s/enum :FI :EN), zip :- s/Int]
                  (ok {:country country, :zip zip}))
                (GET "/api/ping" []
                  (moved-permanently
                    (path-for :lost {:country :FI, :zip 33200}))))]
      (fact "path-for resolution"
        (let [[status body] (GET app "/api/ping" {})]
          status => 200
          body => {:country "FI"
                   :zip 33200}))))

Component integration

Component integration

Exception handling

Exception handling

Schemas

Compojure-api uses the Schema library to describe data models, backed up by ring-swagger for mapping the models into Swagger JSON Schemas. With Map-based schemas, Keyword keys should be used instead of Strings.

Coercion

Models, routes and meta-data

The enhanced route-macros allow you to define extra meta-data by adding a) meta-data as a map or b) as pair of keyword-values in Liberator-style. Keys are used as a dispatch value into restructure multimethod, which generates extra code into the endpoints. If one tries to use a key that doesn't have a dispatch function, a compile-time error is raised.

There are a number of available keys in the meta namespace, which are always available. These include:

  • input & output schema definitions (with automatic coercion and swagger-data extraction)
  • extra swagger-documentation like :summary, :description, :tags

One can also easily create one's own dispatch handlers, just add a new dispatch function to the multimethod.

(s/defschema User {:name s/Str
                   :sex (s/enum :male :female)
                   :address {:street s/Str
                             :zip s/Str}})

(POST "/echo" []
  :summary "echoes a user from a body" ; for swagger-documentation
  :body [user User]                    ; validates/coerces the body to be User-schema, assigns it to user (lexically scoped for the endpoint body) & generates the needed swagger-docs
  :return User                         ; validates/coerces the 200 response to be User-schema, generates needed swagger-docs
  (ok user))                           ; the body itself.

Everything happens at compile-time, so you can macroexpand the previous to learn what happens behind the scenes.

More about models

You can also wrap models in containers (vector and set) and add descriptions:

(POST "/echos" []
  :return [User]
  :body [users (describe #{Users} "a set of users")]
  (ok users))

Schema-predicate wrappings work too:

(POST "/nachos" []
  :return (s/maybe {:a s/Str})
  (ok nil))

And anonymous schemas:

  (PUT "/echos" []
    :return   [{:id Long, :name String}]
    :body     [body #{{:id Long, :name String}}]
    (ok body))

Query, Path, Header and Body parameters

All parameters can also be destructured using the Plumbing syntax with optional type-annotations:

(GET "/sum" []
  :query-params [x :- Long, y :- Long]
  (ok {:total (+ x y)}))

(GET "/times/:x/:y" []
  :path-params [x :- Long, y :- Long]
  (ok {:total (* x y)}))

(POST "/divide" []
  :return Double
  :form-params [x :- Long, y :- Long]
  (ok {:total (/ x y)}))

(POST "/minus" []
  :body-params [x :- Long, y :- Long]
  (ok {:total (- x y)}))

(POST "/power" []
  :header-params [x :- Long, y :- Long]
  (ok {:total (long (Math/pow x y))})

Note: Destructuring syntax has a restriction that the parameter names must be representable as Clojure symbols. This means that for example x.foo parameter is not usable with destructuring. You can instead use non -params versions of the restructuring handlers.

Returning raw values

Raw values / primitives (e.g. not sequences or maps) can be returned when a :return -metadata is set. Swagger, ECMA-404 and ECMA-262 allow this (while RFC4627 forbids it).

note setting a :return value as String allows you to return raw strings (as JSON or whatever protocols your app supports), as opposed to the Ring Spec.

(context "/primitives" []

  (GET "/plus" []
    :return       Long
    :query-params [x :- Long {y :- Long 1}]
    :summary      "x+y with query-parameters. y defaults to 1."
    (ok (+ x y)))

  (GET "/datetime-now" []
    :return org.joda.time.DateTime
    :summary "current datetime"
    (ok (org.joda.time.DateTime.)))

  (GET "/hello" []
    :return String
    :query-params [name :- String]
    :description "<h1>hello world.</h1>"
    :summary "echoes a string from query-params"
    (ok (str "hello, " name))))

Response-models

Key :responses takes a map of HTTP status codes to Schema definitions maps (with optional :schema, :description and :headers keys). :schema defines the return model and gets automatic coercion for it. If a route tries to return an invalid response value, an InternalServerError is raised with the schema validation errors.

(GET "/" []
  :query-params [return :- (s/enum :200 :301 :403 :404)]
  :responses    {301 {:schema s/Str, :description "new place!", :headers {:location s/Str}}
                 403 {:schema {:code s/Str}, :description "spiders?"}}
                 404 {:schema {:reason s/Str}, :description "lost?"}
  :return       Total
  :summary      "multiple returns models"
  (case return
    :200 (ok {:total 42})
    :301 (moved-permanently "http://www.new-total.com")
    :403 (forbidden {:code "forest"})
    :404 (not-found {:reason "lost"})))

The :return maps the model just to the response 200, so one can also say:

(GET "/" []
  :query-params [return :- (s/enum :200 :403 :404)]
  :responses    {200 {:schema Total, :description "happy path"}
                 403 {:schema {:code s/Str}, :description "spiders?"}}
                 404 {:schema {:reason s/Str}, :description "lost?"}}
  :summary      "multiple returns models"
  (case return
    :200 (ok {:total 42})
    :403 (forbidden {:code "forest"})
    :404 (not-found {:reason "lost"})))

There is also a :default status code available, which stands for "all undefined codes".

I just want the swagger-docs, without coercion

You can either use the normal restructuring (:query, :path etc.) to get the swagger docs and disable the coercion with:

(api
  :coercion (constantly nil)
  ...

or use the :swagger restructuring at your route, which pushes the swagger docs for the routes:

(GET "/route" [q]
  :swagger {:x-name :boolean
            :operationId "echoBoolean"
            :description "Echoes a boolean"
            :parameters {:query {:q s/Bool}}}
  ;; q might be anything here.
  (ok {:q q}))

Swagger-aware File-uploads

Swagger-aware File-uploads

Route-specific middleware

Key :middleware takes a vector of middleware to be applied to the route. Note that the middleware don't see any restructured bindings from within the route body. They are executed inside the route so you can safely edit request etc. and the changes won't leak to other routes in the same context.

 (DELETE "/user/:id" []
   :middleware [audit-support (for-roles :admin)]
   (ok {:name "Pertti"}))

Creating your own metadata handlers

Creating your own metadata handlers