- Notifications
You must be signed in to change notification settings - Fork147
Sweet web apis with Compojure & Swagger
License
metosin/compojure-api
Folders and files
| Name | Name | Last commit message | Last commit date | |
|---|---|---|---|---|
Repository files navigation
Stuff on top ofCompojure for making sweet web apis.
- API Docs
- Schema for input & output data coercion
- Swagger 2.0 for api documentation, viaring-swagger
- simple extendable DSL viametadata handlers
- bundled middleware for common api behavior (exception mapping, data formats & serialization)
- route macros for putting things together, including theSwagger-UI viaring-swagger-ui
If you are upgrading your existing pre0.20.0 compojure-api app to use0.20.0 or later, you have to migrate the Swagger modelsfrom 1.2 to 2.0. SeeMigration guide for details.
(nsexamples.thingie (:require [ring.util.http-response:refer:all] [compojure.api.sweet:refer:all] [schema.core:as s]));;;; Schemas;;(s/defschemaThingie {:id Long:hot Boolean:tag (s/enum:kikka:kukka):chief [{:name String:type #{{:id String}}}]});;;; Routes;;(defroutes*legacy-route (GET*"/legacy/:value" [value] (ok {:value value})))(defapiapp (swagger-ui) (swagger-docs {:info {:title"Sample api"}}) (GET*"/" []:no-doctrue (ok"hello world")) (context*"/api" []:tags ["thingie"] (GET*"/plus" []:return Long:query-params [x:- Long, {y:- Long1}]:summary"x+y with query-parameters. y defaults to 1." (ok (+ x y))) (POST*"/minus" []:return Long:body-params [x:- Long, y:- Long]:summary"x-y with body-parameters." (ok (- x y))) (GET*"/times/:x/:y" []:return Long:path-params [x:- Long, y:- Long]:summary"x*y with path-parameters" (ok (* x y))) (POST*"/divide" []:return Double:form-params [x:- Long, y:- Long]:summary"x/y with form-parameters" (ok (/ x y))) (GET*"/power" []:return Long:header-params [x:- Long, y:- Long]:summary"x^y with header-parameters" (ok (long (Math/pow x y)))) legacy-route (PUT*"/echo" []:return [{:hot Boolean}]:body [body [{:hot Boolean}]]:summary"echoes a vector of anonymous hotties" (ok body)) (POST*"/echo" []:return (s/maybe Thingie):body [thingie (s/maybe Thingie)]:summary"echoes a Thingie from json-body" (ok thingie))) (context*"/context" []:tags ["context*"]:summary"summary inherited from context" (context*"/:kikka" []:path-params [kikka:- s/Str]:query-params [kukka:- s/Str] (GET*"/:kakka" []:path-params [kakka:- s/Str] (ok {:kikka kikka:kukka kukka:kakka kakka})))))
To try it yourself, clone this repository and do either:
lein runlein repl&(go)
Clone theexamples-repository.
Use a Leiningen template, with or without tests:
lein new compojure-api my-apilein new compojure-api my-api +midjelein new compojure-api my-api +clojure-testThere is prepackaged middlewarecompojure.api.middleware/api-middleware for common web api usage. It's a enhanced version ofcompojure.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-paramsandring.middleware.format-response/wrap-restful-response- default supported protocols are:
:json-kw,:yaml-kw,:edn,:transit-jsonand:transit-msgpack - enabled protocol support is also published into Swagger docs via
ring.swagger.middleware/wrap-swagger-data.
- default supported protocols are:
All middlewares are preconfigured with good/opinionated defaults, but one can override the configurations by passing a options Map into theapi-middleware. Seeapi-docs for details.
To get all the benefits of Compojure-api, one should wrap the apis intocompojure.api.core/api-macro. It is responsible for creating and publishingthe compile-time route-tree from your api, enabling the Swagger documentation. It takes an optional map as a first parameter which is passed directlyto the underlayingapi-middleware for configuring the used middlewares.
(nsexample.handler (:require [ring.util.http-response:refer [ok]] [compojure.api.core:refer:all] [compojure.core:refer:all]))(defapp (api {:formats [:json-kw]} (context"/api" [] (GET"/ping" [] (ok {:ping"pong"}))))) (slurp (:body (app {:request-method:get:uri"/api/ping"}))); => "{\"ping\":\"pong\"}"
compojure.api.core/defapi is just a shortcut for defining an api:
(defapiapp {:formats [:json-kw]} (context"/api" [] (GET"/ping" [] (ok {:ping"pong"}))))
To help setting up custom middleware there is acompojure.api.core/middlewares macro:
(require '[ring.middleware.head:refer [wrap-head]])(defapiapp (middlewares [wrap-head] (context"/api" [] (GET"/ping" [] (ok {:ping"pong"})))))
If you want to wrap you complete app in some middlewares,defapi returns handler which you can wrap just like with regulardefroutes:
(defapiapp'...)(defapp (-> app' (wrap-head)))
One can use eithervanilla Compojure routes or their enhanced versions fromcompojure.api.core.Enhanced versions have* in their name (GET*,POST*,context*,defroutes* etc.) so that they don't get mixed up with the originals.Enhanced version can be used exactly as their ancestors but also allow new features via extendable meta-data handlers.
Namespacecompojure.api.sweet is a public entry point for all routing macros. It imports the enchanced route macros fromcompojure.api.core,swagger-stuff fromcompojure.api.swagger and few extras fromcompojure.core.
There is alsocompojure.api.legacy namespace which contains rest of the public vars fromcompojure.core (theGET,POST etc. endpoint macros which are not contained insweet). Usingsweet in conjunction withlegacy should provide a drop-in-replacement forcompojure.core - with new new route goodies.
(nsexample.handler2 (:require [ring.util.http-response:refer [ok]] [compojure.api.sweet:refer:all]))(defapiapp (context*"/api" [] (GET*"/user/:id" [id] (ok {:id id})) (POST*"/echo" {body:body-params} (ok body))))
Compojure-api usesSwagger for route documentation.
Enabling Swagger route documentation in your application is done by:
- Wrap your api-applicaiton into an
api(ordefapi).- uses macro-peeling & source linking to reconstruct the route tree from route macros at macro-expansion time (~no runtime penalty)
- if you intend to split your routes behind multiple Vars via
defroutes, usedefroutes*instead so that their routes get also collected.Note: since0.20.0thedefroutes*are automatically referenced over a Var to get smoother development flow. - Add
:no-docmetadata to any routes you don't want to appear in the documentation
- Add
compojure.api.swagger/swagger-docsroute to publish the swagger spec - optionally Mount
compojure.api.swagger/swagger-uito add theSwagger-UI to the web app.
If the embedded (Ring-)Swagger-UI isn't enough for you, you can exclude it from dependencies and create & package your own UI from thesources:
[metosin/compojure-api"0.20.3":exclusions [metosin/ring-swagger-ui]]
(nsexample.handler3 (:require [ring.util.http-response:refer [ok]] [compojure.api.sweet:refer:all]))(defroutes*legacy-route (GET*"/ping/:id" [id] (ok {:id id})))(defapiapp (swagger-ui) (swagger-docs) (context"/api" [] legacy-route (POST*"/echo" {body:body-params} (ok body))))
The above sample application mounts swagger-docs to root/ and serves the swagger-docs from/swagger.json.
The resulting swagger-spec data (published by theswagger-docs) is combined from three sources:
- Compile-time route & schema information, generated for you by the lib
- Run-time extra information from the middlewares, passed in with the request
- User-set custom information
Passed in automatically via request injection.
By default, the application wire-format serialization capabilities (:produces and:consumes)are injected in automatially by theapi machinery.
One can contribute extra arbitrary swagger-data (like swagger security definitions) to the docs viaring.swagger.middleware/wrap-swagger-data middleware.
Theswagger-docs can be used without parameters, but one can set any valid root-level Swagger Data via it.
(swagger-docs)(swagger-docs {:info {:version"1.0.0":title"Sausages":description"Sausage description":termsOfService"http://helloreverb.com/terms/":contact {:name"My API Team":email"foo@example.com":url"http://www.metosin.fi"}:license {:name"Eclipse Public License":url"http://www.eclipse.org/legal/epl-v10.html"}}:tags [{:name"kikka",:description"kukka"}]})
See theSwagger-spec for more details.
As one might accidentally pass invalid swagger data in, you should validate the end results.Seewiki for details.
One can configure Ring-Swagger by providing options toapi-middleware for key:ring-swagger. SeeRing-Swagger docs for possible options and examples.
(defapiapp {:ring-swagger {:ignore-missing-mappings?true}}) (swagger-docs) (swagger-ui) ...)
To ensure that your API is valid, one can callcompojure.api.swagger/validate. It takes the api (the ring handler returned byapi ordefapi) as an parameter and returns the api of throws an Exception. The validation does the following:
- if the api is not an swagger api (does not the
swagger-docsmounted) and compiles, it's valid - if the api is an swagger api (does have the
swagger-docsmounted):- Ring Swagger is called to verify that all Schemas can be transformed to Swagger JSON Schemas
- the swagger-spec endpoint is called with 200 responses status
(require '[compojure.api.sweet:refer:all])(require '[compojure.api.swagger:refer [validate])(defrecordNonSwaggerRecord [data])(defapp (validate (api (swagger-docs) (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
TODO: optionallyvalidate the swagger spec itself againt the JSON Schema.
Inspired by the awesomebidi, Compojure-api also supports bi-directional routing. Routes can be attached with a:name and other endpoints can refer to them viapath-for macro (orpath-for* function).path-for takes the route-name and optionally a mapof path-parameters needed to construct the full route. Normal ring-swagger path-parameter serialization is used, so one can use all supported Schemaelements as the provided parameters.
Route names should be keywords. Compojure-api ensures that there are no duplicate endpoint names within anapi, raising aIllegalArgumentExceptionat compile-time if it founds multiple routes with 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,:zip33200}))))] (fact"path-for resolution" (let [[status body] (get* app"/api/ping" {})] status =>200 body => {:country"FI":zip33200}))))
Stuert Sierra'sComponent is a great library for managing the statefulresources of your app. There areseveral strategiesto use it. Here are some samples how to use Component with compojure-api:
(defncreate-handler [{:keys [db]:as system}] (api (swagger-docs) (swagger-ui) (GET*"/user/:id" []:path-params [id:- s/Str] (ok (get-user db id)))))
Use either:components-option ofapi-middleware orwrap-components-middlewareto associate the components with your API.
Components can be read from the request usingcompojure.api.middleware/get-components or usingthe:components restucturing with letk-syntax.
(require '[compojure.api.middleware:as mw])(defapihandler (GET*"/user/:id" []:path-params [id:- s/Str]:components [db] (ok (get-user db id))))(defnapp (mw/wrap-components handler (create-system))
(defapiapp {:components (create-system)} (GET*"/user/:id" []:path-params [id:- s/Str]:components [db] (ok (get-user db id))))
To see this in action, trylein run and navigate to Components api group.
All exceptions should be handled gracefully. Compojure-api ships with customizable exception handling with gooddefaults. Customization is done viaapi options - delegating tocompojure.api.middleware/wrap-exceptions, whichdoes the real work. It catches all thrown exceptions and selects a custom handler based on the thrown exceptionex-data or Slingshot value of key:type. If an exception doesn't have ex-data (e.g. legacy Java Exceptions),:compojure.api.exception/default type is used. Exception handlers are 3-arity functions, getting the exception,ex-data and request as arguments. Below are the default type definitions and default handling:
| type | what | default |
|---|---|---|
:compojure.api.exception/request-parsing | Input data de-serialization errors. | 400 + error in body |
:compojure.api.exception/request-validation | Request Schema coercion errors. | 400 + schema error in body |
:compojure.api.exception/response-validation | Response Schema coercion errors. | 500 + schema error in body |
:compojure.api.exception/default | Everything else. | 500 + print stacktrace + safe message |
example to override the default case + add a custom exception type + handler for it:
(defncustom-handler [^Exception e data request] (internal-server-error {:message (.getMessage e)}))(defncalm-handler [^Exception e data request] (enhance-your-calm {:message (.getMessage e),:data data}))(defapi {:exceptions {:handlers {:compojure.api.exception/default custom-handler::calm calm-handler}}} (GET*"/bang" [] (throw (RuntimeException."kosh"))) (GET*"/calm" [] (throw (ex-info"fail" {:type::calm,:oil"snake"}))))
Compojure-api uses theSchema to describe data models, backed up byring-swagger for mapping the models int Swagger JSON Schemas.With Map-based schemas, Keyword keys should be used instead of Strings.
Input and output schemas are coerced automatically using a schema coercion matcher linked to a coercion type.There are three types of coercion and currently two different coercion matchers available (from Ring-Swagger).
The following table provides the default mapping from type -> coercion matcher.
| type | default coercion matcher | used with |
|---|---|---|
:body | json-schema-coercion-matcher | request body |
:string | query-schema-coercion-matcher | query, path, header and form parameters |
:response | json-schema-coercion-matcher | response body |
One can override the default coercion behavior by providing a coercion function of typering-request->coercion-type->coercion-matcher either by:
- api-middleware option
:coercion - route-level restructuring
:coercion
As the coercion function takes in the ring-request, one can select coercion matcher based on the user selected wireformat or any other header. The plan is to provide extendable protocol-based coercion out-of-the-box (Transit doesn'tneed any coercion, XML requires some extra love with sequences). Stay tuned.
Examples on overriding the default coercion can found in thethe tests.
All coercion code uses the internallyring.swagger.schema/coerce!, which throws managed exceptions when a valuecan't be coerced. Theapi-middleware catches these exceptions and returns the validation error as serializableClojure data structure, sent to the client.
One can also callring.swagger.schema/coerce! manually:
(require '[ring.swagger.schema:refer [coerce!])(require '[schema.core:as s])(s/defschemaThingie {:id Long:tag (s/enum:kikka:kukka)})(coerce! Thingie {:id123,:tag"kikka"}); => {:id 123 :tag :kikka}(coerce! Thingie {:id123,:tags"kakka"}); => ExceptionInfo throw+: {:type :ring.swagger.schema/validation, :error {:tags disallowed-key, :tag missing-required-key}} ring.swagger.schema/coerce! (schema.clj:88)
The enhanced route-macros allow you to define extra meta-data by adding a) meta-data as a map or b) as pair ofkeyword-values in Liberator-style. Keys are used as a dispatch value intorestructure 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 is lot's of available keys inmetanamespace, 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 own dispatch handlers, just add new dispatch function to the multimethod.
(s/defschemaUser {: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, assignes it to user (lexically scoped for the endpoint body) & generated 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.
You can also wrap models in containers (vector andset) and add descriptions:
(POST*"/echos" []:return [User]:body [users (describe #{Users}"a set on users")] (ok users))
Schema-predicate wrappings work too:
(POST*"/nachos" []:return (s/maybe {:a s/Str}) (oknil))
And anonymous schemas:
(PUT*"/echos" []:return [{:id Long,:name String}]:body [body #{{:id Long,:name String}}] (ok body))
All parameters can also be destructured using thePlumbing 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))})
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 asString allows you to return raw strings (as JSON or whatever protocols yourapp supports), opposed to theRing Spec.
(context"/primitives" [] (GET*"/plus" []:return Long:query-params [x:- Long {y:- Long1}]: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]:notes"<h1>hello world.</h1>":summary"echoes a string from query-params" (ok (str"hello," name))))
Key:responses takes a map of http-status-code to schema-definitions map(with optional:schema,:description and:headers keys).:schema defines the return modeland get's automatic coercion for it. If a route tries to return an invalid response value,anInternalServerError is raised with the schema validation errors.
(GET*"/" []:query-params [return:- (s/enum:200:403:404)]:responses {403 {:schema {:code s/Str},:description"spiders?"}}404 {:schema {:reason s/Str},:description"lost?"}}:return Total:summary"multiple returns models" (case return:200 (ok {:total42}):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 {:total42}):403 (forbidden {:code"forest"}):404 (not-found {:reason"lost"})))
There is also a:default status code available, which stands for "all undefined codes".
You can either use the normal restructuring (:query,:path etc.) to get the swagger docs anddisable the coercion with:
(api:coercion (constantlynil) ...
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"Ehcoes a boolean":parameters {:query {:q s/Bool}}};; q might be anything here. (ok {:q q}))
Mostly provided by Ring-Swagger. Restructuring:multipart-params pushes alsomultipart/form-data as the onlyavailable consumption.
;; versions before 0.23.0(require '[compojure.api.upload:as upload]);; versions 0.23.0+(require '[ring.swagger.upload:as upload])(POST*"/upload" []:multipart-params [file:- upload/TempFileUpload]:middlewares [upload/wrap-multipart-params] (ok (dissoc file:tempfile)))
Key:middlewares takes a vector of middlewares to be applied to the route.Note that the middlewares 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 changeswon't leak to other routes in the same context.
(DELETE*"/user/:id" []:middlewares [audit-support (for-roles:admin)] (ok {:name"Pertti"}))
Compojure-api handles the route metadata by calling the multimethodcompojure.api.meta/restructure-param withmetadata key as a dispatch value.
Multimethods take three parameters:
- metadata key
- metadata value
- accumulator map with keys
:lets, a vector of let bindings applied first before the actual body:letks, a vector of letk bindings applied second before the actual body:middlewares, a vector of route specific middlewares (applied from left to right):parameters, meta-data of a route (without the key & value for the current multimethod):body, a sequence of the actual route body
.. and should return the modified accumulator. Multimethod calls are reduced to produce the final accumulator forcode generation. Defined key-value -based metadatas for routes are guaranteed to run on top-to-bottom order of the soall the potentiallet andletk variable overrides can be solved by the client. Default implementation is to keepthe key & value as a route metadata.
You can add your own metadata-handlers by implementing the multimethod:
(defmethodcompojure.api.meta/restructure-param:auth [_ token {:keys [parameters lets body middlewares]:as acc}]"Make sure the request has X-AccessToken header and that it's value is 123. Binds the value into a variable" (-> acc (update-in [:lets] into [{{token"x-accesstoken"}:headers} '+compojure-api-request+]) (assoc:body `((if (= ~token"123") (do ~@body) (ring.util.http-response/forbidden"Auth required"))))))
Using it:
(GET*"/current-session" []:auth token (ok {:token token}))
macroexpanding-1 it too see what's get generated:
(clojure.pprint/pprint (macroexpand-1 `(GET*"/current-session" []:auth token (ok {:token token}))))(compojure.core/GET"/current-session" [:as +compojure-api-request+] (clojure.core/let [{{examples.thingie/token"x-accesstoken"}:headers} +compojure-api-request+] (do (if (clojure.core/= examples.thingie/token"123") (do (ring.util.http-response/ok {:token examples.thingie/token})) (ring.util.http-response/forbidden"Auth required")))))
- UsingBuddy with Compojure-api:https://gist.github.com/Deraen/ef7f65d7ec26f048e2bb
Copyright © 2014-2015Metosin Oy
Distributed under the Eclipse Public License, the same as Clojure.
About
Sweet web apis with Compojure & Swagger
Topics
Resources
License
Contributing
Uh oh!
There was an error while loading.Please reload this page.
Stars
Watchers
Forks
Packages0
Uh oh!
There was an error while loading.Please reload this page.