Metosin

Compojure-api 1.0.0 Is Out

After two years of the first public version, we are proud to present Compojure-api 1.0.0. It's a big rewrite, removing most of the nonsense (like compile-time route peeling) and cleaning up the public apis. As a result, the development flow is much smoother and it looks and tastes more like the Compojure itself, allowing one to structure the apps as intended.

Changelog covers all the breaking changes from previous versions and there is a migration guide + compile-time migration assertions for existing library users. Not Elm-grade thou.

Quick look

Compojure-api is a web api library for Clojure, standing on top of the awesome Compojure. It add's Schema-based input & output validation, swagger-support, transparent support for common wire-formats (JSON, EDN, YAML, Transit-json & Transit-MessagePack), better exception handling and bi-directional routing. It is also extendable, offering a large set of api options (with sane defaults) and a restructuring-api for extending the route DSL.

Using it

Normally, the only namespace needed to import is compojure.api.sweet. It contains the top-level api function for creating the api-routes, defapi as a shortcut, enchanced versions of most the Compojure route-macros (GET, POST, context, routes etc.) plus some other utility functions and macros.

Writing Compojure-style apps should be pretty straightforward:

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

(defapi app
  (GET "/hello" [name]
    (ok {:message (str "Hello, " name)})))

The enchanced route-macros allow a restructuring map or a sequence of keyword keys and values before the actual body. Values are dispatched at compile-time with a multimethod compojure.api.meta/restructure, allowing new code to be injected into the route. Compojure-api ships with a large number of predefined restucturing handlers.

Example endpoint

(GET "/plus" []
  :return {:result Long}
  :query-params [x :- Long, y :- Long]
  :summary "adds two numbers together"
  (ok {:result (+ x y)}))

The code snippet creates a GET /plus route, which expects query-paramers x and y, which are automatically coerced into Longs and available in the body of the route. If case of invalid parameters, a schema error is thrown - translating into 400 bad-request (with validation errors as the body). Return format is coerced to a {:result Long} or an schema server error is raised, translating into a 500 internal-server-error.

Both the coercion behavior and exception handling can be overridden either at route or at api-level.

Evaluating the previous route in a REPL, one can see the internal representation of it. The returned Route-records is also Invokable, so can be used just like a regular ring-handler.

#compojure.api.routes.Route
  {:path "/plus"
   :method :get
   :info {:responses {200 {:schema {:result java.lang.Long}
                                    :description ""}}
          :parameters {:query {:y java.lang.Long
                               :x java.lang.Long
                               Keyword Any}}
           :summary "adds two numbers together"}
   :childs nil
   :handler #object[...]}

Closures

One of the key features of Compojure is the use of closures in routing. One can destructure an request at mid-route and use the destuctured values in sub-routes. Same applies for Compojure-api restructuring.

(defn admin-routes [db]
  (context "/admin" []
    :tags ["admin"]
    :middleware [[wrap-require-role :admin]]

    (context "/thingies/:id" []
      :path-params [id :- Long]

      (GET "/" []
        :summary "load a thingie from a db"
        (thingie/load db id))

      (DELETE "/" []
        :summary "removes a thingie from a db"
        (thingies/delete db id)))))

In the example, all sub-routes inherit the "admin"-tag and all sub-routes can access the coerced path-parameter id. The sub-routes are also guarded by a (user-defined) middleware that checks that the current for the :admin role. The :middleware uses Duct-style data-driven syntax in 1.0.0.

Swagger docs

Compojure-api uses Ring-Swagger for api documentation and Schema coercion. Both libraries have been developed in sync and work seamlessly together. To enable swagger-docs to an api, one need either to mount swagger-routes handler to the api or use api-options to configure the swagger artifacts.

Simplest possible swagger-enabled app

(api
  (swagger-routes)
  (GET "/ping" []
    (ok {:ping "pong"})))

More complete application

(ns sample
  (:require [compojure.api.sweet :refer :all]
            [ring.util.http-response :refer :all]
            [schema.core :as s]))

(s/defschema Pizza
  {:name s/Str
   (s/optional-key :description) s/Str
   :size (s/enum :L :M :S)
   :origin {:country (s/enum :FI :PO)
            :city s/Str}})

(def app
  (api
    {:swagger
     {:ui "/api-docs"
      :spec "/swagger.json"
      :data {:info {:title "Sample API"
                    :description "Compojure Api example"}
             :tags [{:name "api", :description "some apis"}]}}}

    (context "/api" []
      :tags ["api"]

      (GET "/plus" []
        :return {:result Long}
        :query-params [x :- Long, y :- Long]
        :summary "adds two numbers together"
        (ok {:result (+ x y)}))

      (POST "/echo" []
        :return Pizza
        :body [pizza Pizza]
        :summary "echoes a Pizza"
        (ok pizza)))))

The previous app produces the following swagger-ui

To play with the previous example yourself, get yourself a Leiningen, go to the command line an type:

lein new compojure-api thingie
cd thingie
lein ring server

The basic template uses lein-ring to be newbie-friendly, there are more advanced examples in the repo.

Future

With the release of 1.0.0 the public apis are mostly finalized and the library is left open for extensions: many things people have asked for can already be done in the client projects using the current extension mechanisms: api options, custom middleware and custom resturcturing.

The top feature wishes (with a potential solution) currently are:

  1. Secure by default
    • bundle with ring-defaults
    • pre-integrate Buddy or Friend
    • swagger-aware authenticaiton & authorization (to Ring-Swagger)
  2. Better support for Liberator
    • 1.0.0 routing is extendable for this...
  3. Better docs

The last thing - wiki should cover more ground, there should be a Q&A and we could have more sample projects &/ templates for common things like Component, Mount and Buddy. Feel free to contribute :)

Related, Dmitri's new book, Web Development with Clojure will walk through parts of compojure-api, looking forward to it.

Final words

It has been great fun developing the Schema/Swagger -things for Clojure for the last two years. Compojure-api started as an small macro-experiment, but has evolved into a feature-rich api library. It's also first of our libraries to go to 1.0.0, probably more to follow. There are currently over 65k downloads, so other people have found that too. Big thanks to all contributors and users for feedback!

If you need help, you can find us from Slack on #ring-swagger or from IRC/Freenode - just say compojure-api or ring-swagger at #clojure and we'll try to help. You can also write an issue to Github and make the wiki better yourself.

Sweet apis.

on behalf of the Metosin Crew,

Tommi