diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 946cce48..4665c66a 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -14,7 +14,7 @@ jobs:
   test:
     strategy:
       matrix:
-        jdk: [8, 11, 17, 21]
+        jdk: [8, 11, 17, 21, 22]
 
     name: Java ${{ matrix.jdk }}
 
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e93d4be7..bfd87589 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,10 @@
 See also: [compojure-api 1.1.x changelog](./CHANGELOG-1.1.x.md)
 
+## Next
+* Lazily load spec and schema coercion
+* bump spec-tools to 0.10.6
+  * notable changes: swagger `:name` defaults to `"body"` instead of `""` ([diff](https://github.com/metosin/spec-tools/compare/0.10.2...0.10.3))
+
 ## 2.0.0-alpha34-SNAPSHOT
 * **BREAKING CHANGE**: `:formatter :muuntaja` sometimes required for `api{-middleware}` options
   * to prepare for 1.x compatibility, :muuntaja must be explicitly configured
diff --git a/project.clj b/project.clj
index e77fe64e..e08a7716 100644
--- a/project.clj
+++ b/project.clj
@@ -12,7 +12,7 @@
                  [com.fasterxml.jackson.datatype/jackson-datatype-joda "2.10.1"]
                  [ring/ring-core "1.8.0"]
                  [compojure "1.6.1" ]
-                 [metosin/spec-tools "0.10.0"]
+                 [metosin/spec-tools "0.10.6"]
                  [metosin/ring-http-response "0.9.1"]
                  [metosin/ring-swagger-ui "3.24.3"]
                  [metosin/ring-swagger "1.0.0"]
@@ -37,7 +37,6 @@
                                   [org.clojure/core.async "0.6.532"]
                                   [javax.servlet/javax.servlet-api "4.0.1"]
                                   [peridot "0.5.2"]
-                                  [com.rpl/specter "1.1.3"]
                                   [com.stuartsierra/component "0.4.0"]
                                   [expound "0.8.2"]
                                   [metosin/jsonista "0.2.5"]
@@ -61,6 +60,9 @@
                                       [org.slf4j/jul-to-slf4j "1.7.30"]
                                       [org.slf4j/log4j-over-slf4j "1.7.30"]
                                       [ch.qos.logback/logback-classic "1.2.3" ]]}
+             :1.10 {:dependencies [[org.clojure/clojure "1.10.1"]]}
+             :1.11 {:dependencies [[org.clojure/clojure "1.11.3"]]}
+             :1.12 {:dependencies [[org.clojure/clojure "1.12.0-alpha11"]]}
              :async {:jvm-opts ["-Dcompojure-api.test.async=true"]
                      :dependencies [[manifold "0.1.8" :exclusions [org.clojure/tools.logging]]]}}
   :eastwood {:namespaces [:source-paths]
@@ -86,7 +88,7 @@
                   ["change" "version" "leiningen.release/bump-version"]
                   ["vcs" "commit"]
                   ["vcs" "push"]]
-  :aliases {"all" ["with-profile" "dev:dev,async"]
+  :aliases {"all" ["with-profile" "dev:dev,async:dev,1.10:dev,1.11:dev,1.12"]
             "start-thingie" ["run"]
             "aot-uberjar" ["with-profile" "uberjar" "do" "clean," "ring" "uberjar"]
             "test-ancient" ["test"]
diff --git a/src/compojure/api/coerce.clj b/src/compojure/api/coerce.clj
new file mode 100644
index 00000000..5a147a14
--- /dev/null
+++ b/src/compojure/api/coerce.clj
@@ -0,0 +1,67 @@
+;; 1.1.x
+(ns compojure.api.coerce
+  (:require [schema.coerce :as sc]
+            [compojure.api.middleware :as mw]
+            [compojure.api.exception :as ex]
+            [clojure.walk :as walk]
+            [schema.utils :as su]
+            [linked.core :as linked]))
+
+(defn memoized-coercer
+  "Returns a memoized version of a referentially transparent coercer fn. The
+  memoized version of the function keeps a cache of the mapping from arguments
+  to results and, when calls with the same arguments are repeated often, has
+  higher performance at the expense of higher memory use. FIFO with 10000 entries.
+  Cache will be filled if anonymous coercers are used (does not match the cache)"
+  []
+  (let [cache (atom (linked/map))
+        cache-size 10000]
+    (fn [& args]
+      (or (@cache args)
+          (let [coercer (apply sc/coercer args)]
+            (swap! cache (fn [mem]
+                           (let [mem (assoc mem args coercer)]
+                             (if (>= (count mem) cache-size)
+                               (dissoc mem (-> mem first first))
+                               mem))))
+            coercer)))))
+
+(defn cached-coercer [request]
+  (or (-> request mw/get-options :coercer) sc/coercer))
+
+(defn coerce-response! [request {:keys [status] :as response} responses]
+  (-> (when-let [schema (or (:schema (get responses status))
+                            (:schema (get responses :default)))]
+        (when-let [matchers (mw/coercion-matchers request)]
+          (when-let [matcher (matchers :response)]
+            (let [coercer (cached-coercer request)
+                  coerce (coercer schema matcher)
+                  body (coerce (:body response))]
+              (if (su/error? body)
+                (throw (ex-info
+                        (str "Response validation failed: " (su/error-val body))
+                        (assoc body :type ::ex/response-validation
+                               :response response)))
+                (assoc response
+                  :compojure.api.meta/serializable? true
+                  :body body))))))
+      (or response)))
+
+(defn body-coercer-middleware [handler responses]
+  (fn [request]
+    (coerce-response! request (handler request) responses)))
+
+(defn coerce! [schema key type request]
+  (let [value (walk/keywordize-keys (key request))]
+    (if-let [matchers (mw/coercion-matchers request)]
+      (if-let [matcher (matchers type)]
+        (let [coercer (cached-coercer request)
+              coerce (coercer schema matcher)
+              result (coerce value)]
+          (if (su/error? result)
+            (throw (ex-info
+                     (str "Request validation failed: " (su/error-val result))
+                     (assoc result :type ::ex/request-validation)))
+            result))
+        value)
+      value)))
diff --git a/src/compojure/api/coercion.clj b/src/compojure/api/coercion.clj
index a83a7082..0dd26b04 100644
--- a/src/compojure/api/coercion.clj
+++ b/src/compojure/api/coercion.clj
@@ -3,8 +3,9 @@
             [compojure.api.exception :as ex]
             [compojure.api.request :as request]
             [compojure.api.coercion.core :as cc]
-            [compojure.api.coercion.schema]
-            [compojure.api.coercion.spec])
+            ;; side effects
+            compojure.api.coercion.register-schema
+            compojure.api.coercion.register-spec)
   (:import (compojure.api.coercion.core CoercionError)))
 
 (def default-coercion :schema)
diff --git a/src/compojure/api/coercion/register_schema.clj b/src/compojure/api/coercion/register_schema.clj
new file mode 100644
index 00000000..e1e8f993
--- /dev/null
+++ b/src/compojure/api/coercion/register_schema.clj
@@ -0,0 +1,8 @@
+(ns compojure.api.coercion.register-schema
+  (:require [compojure.api.coercion.core :as cc]))
+
+(defmethod cc/named-coercion :schema [_] 
+  (deref
+    (or (resolve 'compojure.api.coercion.schema/default-coercion)
+        (do (require 'compojure.api.coercion.schema)
+            (resolve 'compojure.api.coercion.schema/default-coercion)))))
diff --git a/src/compojure/api/coercion/register_spec.clj b/src/compojure/api/coercion/register_spec.clj
new file mode 100644
index 00000000..143320fb
--- /dev/null
+++ b/src/compojure/api/coercion/register_spec.clj
@@ -0,0 +1,8 @@
+(ns compojure.api.coercion.register-spec
+  (:require [compojure.api.coercion.core :as cc]))
+
+(defmethod cc/named-coercion :spec [_]
+  (deref
+    (or (resolve 'compojure.api.coercion.spec/default-coercion)
+        (do (require 'compojure.api.coercion.spec)
+            (resolve 'compojure.api.coercion.spec/default-coercion)))))
diff --git a/src/compojure/api/coercion/schema.clj b/src/compojure/api/coercion/schema.clj
index b308d0c2..9a7e01b0 100644
--- a/src/compojure/api/coercion/schema.clj
+++ b/src/compojure/api/coercion/schema.clj
@@ -5,7 +5,9 @@
             [compojure.api.coercion.core :as cc]
             [clojure.walk :as walk]
             [schema.core :as s]
-            [compojure.api.common :as common])
+            [compojure.api.common :as common]
+            ;; side effects
+            compojure.api.coercion.register-schema)
   (:import (java.io File)
            (schema.core OptionalKey RequiredKey)
            (schema.utils ValidationError NamedError)))
@@ -84,5 +86,3 @@
   (->SchemaCoercion :schema options))
 
 (def default-coercion (create-coercion default-options))
-
-(defmethod cc/named-coercion :schema [_] default-coercion)
diff --git a/src/compojure/api/coercion/spec.clj b/src/compojure/api/coercion/spec.clj
index 9b20481a..b5d6ad31 100644
--- a/src/compojure/api/coercion/spec.clj
+++ b/src/compojure/api/coercion/spec.clj
@@ -6,7 +6,9 @@
             [clojure.walk :as walk]
             [compojure.api.coercion.core :as cc]
             [spec-tools.swagger.core :as swagger]
-            [compojure.api.common :as common])
+            [compojure.api.common :as common]
+            ;; side effects
+            compojure.api.coercion.register-spec)
   (:import (clojure.lang IPersistentMap)
            (schema.core RequiredKey OptionalKey)
            (spec_tools.core Spec)
@@ -149,5 +151,3 @@
   (->SpecCoercion :spec options))
 
 (def default-coercion (create-coercion default-options))
-
-(defmethod cc/named-coercion :spec [_] default-coercion)
diff --git a/src/compojure/api/middleware.clj b/src/compojure/api/middleware.clj
index ebc603b2..3d6ba644 100644
--- a/src/compojure/api/middleware.clj
+++ b/src/compojure/api/middleware.clj
@@ -8,6 +8,7 @@
             [ring.middleware.keyword-params :refer [wrap-keyword-params]]
             [ring.middleware.nested-params :refer [wrap-nested-params]]
             [ring.middleware.params :refer [wrap-params]]
+            [ring.swagger.coerce :as coerce]
 
             [muuntaja.middleware]
             [muuntaja.core :as m]
@@ -88,6 +89,12 @@
 ;; Options
 ;;
 
+;; 1.1.x
+(defn get-options
+  "Extracts compojure-api options from the request."
+  [request]
+  (::options request))
+
 (defn wrap-inject-data
   "Injects data into the request."
   [handler data]
@@ -108,6 +115,20 @@
     ([request respond raise]
      (handler (coercion/set-request-coercion request coercion) respond raise))))
 
+;; 1.1.x
+(def default-coercion-matchers
+  {:body coerce/json-schema-coercion-matcher
+   :string coerce/query-schema-coercion-matcher
+   :response coerce/json-schema-coercion-matcher})
+
+;; 1.1.x
+(defn coercion-matchers [request]
+  (let [options (get-options request)]
+    (if (contains? options :coercion)
+      (if-let [provider (:coercion options)]
+        (provider request))
+      default-coercion-matchers)))
+
 ;;
 ;; Muuntaja
 ;;
diff --git a/test-suites/compojure1/.gitignore b/test-suites/compojure1/.gitignore
new file mode 100644
index 00000000..eb5a316c
--- /dev/null
+++ b/test-suites/compojure1/.gitignore
@@ -0,0 +1 @@
+target
diff --git a/test-suites/compojure1/.lein-repl-history b/test-suites/compojure1/.lein-repl-history
new file mode 100644
index 00000000..e69de29b
diff --git a/test-suites/compojure1/project.clj b/test-suites/compojure1/project.clj
new file mode 100644
index 00000000..ca1d8bd7
--- /dev/null
+++ b/test-suites/compojure1/project.clj
@@ -0,0 +1,93 @@
+(defproject metosin/compojure-api "1.1.14-SNAPSHOT"
+  :description "Compojure Api"
+  :url "https://github.com/metosin/compojure-api"
+  :license {:name "Eclipse Public License"
+            :url "http://www.eclipse.org/legal/epl-v10.html"
+            :distribution :repo
+            :comments "same as Clojure"}
+  :scm {:name "git"
+        :url "https://github.com/metosin/compojure-api"}
+  :source-paths ["../../src"]
+  :dependencies [[prismatic/schema "1.1.12"]
+                 [prismatic/plumbing "0.5.5"]
+                 [ikitommi/linked "1.3.1-alpha1"] ;; waiting for the original
+                 [metosin/muuntaja "0.6.6"]
+                 [com.fasterxml.jackson.datatype/jackson-datatype-joda "2.10.1"]
+                 [ring/ring-core "1.8.0"]
+                 [compojure "1.6.1" ]
+                 [org.clojure/core.memoize "0.8.2"]
+                 [clj-commons/clj-yaml "0.7.0"]
+                 [org.yaml/snakeyaml "1.24"]
+                 [ring-middleware-format "0.7.4"]
+                 [metosin/spec-tools "0.10.0"]
+                 [metosin/ring-http-response "0.9.1"]
+                 [metosin/ring-swagger-ui "3.24.3"]
+                 [metosin/ring-swagger "0.26.2"]
+
+                 ;; Fix dependency conflicts
+                 [clj-time "0.15.2"]
+                 [joda-time "2.10.5"]
+                 [riddley "0.2.0"]]
+  :profiles {:uberjar {:aot :all
+                       :ring {:handler examples.thingie/app}
+                       :source-paths ["examples/thingie/src"]
+                       :dependencies [[org.clojure/clojure "1.10.1"]
+                                      [http-kit "2.3.0"]
+                                      [reloaded.repl "0.2.4"]
+                                      [com.stuartsierra/component "0.4.0"]]}
+             :dev {:jvm-opts ["-Dcompojure.api.core.allow-dangerous-middleware=true"]
+                   :repl-options {:init-ns user}
+                   :plugins [[lein-clojars "0.9.1"]
+                             [lein-midje "3.2.1"]
+                             [lein-ring "0.12.0"]
+                             [funcool/codeina "0.5.0"]]
+                   :dependencies [[org.clojure/clojure "1.10.1"]
+                                  [slingshot "0.12.2"]
+                                  [peridot "0.5.1"]
+                                  [javax.servlet/servlet-api "2.5"]
+                                  [midje "1.9.9"]
+                                  [com.stuartsierra/component "0.4.0"]
+                                  [reloaded.repl "0.2.4"]
+                                  [http-kit "2.3.0"]
+                                  [criterium "0.4.5"]]
+                   :ring {:handler examples.thingie/app
+                          :reload-paths ["src" "examples/thingie/src"]}
+                   :source-paths ["examples/thingie/src" "examples/thingie/dev-src"]
+                   :main examples.server}
+             :perf {:jvm-opts ^:replace ["-server"
+                                         "-Xmx4096m"
+                                         "-Dclojure.compiler.direct-linking=true"]}
+             :logging {:dependencies [[org.clojure/tools.logging "0.5.0"]]}
+             :1.8 {:dependencies [[org.clojure/clojure "1.8.0"]]}
+             :1.9 {:dependencies [[org.clojure/clojure "1.9.0"]]}
+             :1.10 {:dependencies [[org.clojure/clojure "1.10.1"]]}}
+  :eastwood {:namespaces [:source-paths]
+             :add-linters [:unused-namespaces]}
+  :codeina {:sources ["src"]
+            :target "gh-pages/doc"
+            :src-uri "http://github.com/metosin/compojure-api/blob/master/"
+            :src-uri-prefix "#L"}
+  :deploy-repositories [["snapshot" {:url "https://clojars.org/repo"
+                                     :username [:gpg :env/clojars_user]
+                                     :password [:gpg :env/clojars_token]
+                                     :sign-releases false}]
+                        ["releases" {:url "https://clojars.org/repo"
+                                     :username [:gpg :env/clojars_user]
+                                     :password [:gpg :env/clojars_token]
+                                     :sign-releases false}]]
+  :release-tasks [["clean"]
+                  ["vcs" "assert-committed"]
+                  ["change" "version" "leiningen.release/bump-version" "release"]
+                  ["vcs" "commit"]
+                  ["vcs" "tag" "--no-sign"]
+                  ["deploy" "release"]
+                  ["change" "version" "leiningen.release/bump-version"]
+                  ["vcs" "commit"]
+                  ["vcs" "push"]]
+  :aliases {"all" ["with-profile" "dev:dev,logging:dev,1.10"]
+            "start-thingie" ["run"]
+            "aot-uberjar" ["with-profile" "uberjar" "do" "clean," "ring" "uberjar"]
+            "test-ancient" ["midje"]
+            "perf" ["with-profile" "default,dev,perf"]
+            "deploy!" ^{:doc "Recompile sources, then deploy if tests succeed."}
+                      ["do" ["clean"] ["midje"] ["deploy" "clojars"]]})
diff --git a/test-suites/compojure1/test/compojure/api/coercion_test.clj b/test-suites/compojure1/test/compojure/api/coercion_test.clj
new file mode 100644
index 00000000..7a16f523
--- /dev/null
+++ b/test-suites/compojure1/test/compojure/api/coercion_test.clj
@@ -0,0 +1,200 @@
+(ns compojure.api.coercion-test
+  (:require [compojure.api.sweet :refer :all]
+            [compojure.api.test-utils :refer :all]
+            [midje.sweet :refer :all]
+            [ring.util.http-response :refer :all]
+            [schema.core :as s]
+            [compojure.api.middleware :as mw]))
+
+(defn has-body [expected]
+  (fn [value]
+    (= (second value) expected)))
+
+(defn fails-with [expected-status]
+  (fn [[status body]]
+    (and (= status expected-status) (contains? body :errors))))
+
+(fact "response schemas"
+  (let [r-200 (GET "/" []
+                :query-params [{value :- s/Int nil}]
+                :responses {200 {:schema {:value s/Str}}}
+                (ok {:value (or value "123")}))
+        r-default (GET "/" []
+                    :query-params [{value :- s/Int nil}]
+                    :responses {:default {:schema {:value s/Str}}}
+                    (ok {:value (or value "123")}))
+        r-200-default (GET "/" []
+                        :query-params [{value :- s/Int nil}]
+                        :responses {200 {:schema {:value s/Str}}
+                                    :default {:schema {:value s/Int}}}
+                        (ok {:value (or value "123")}))]
+    (fact "200"
+      (get* (api r-200) "/") => (has-body {:value "123"})
+      (get* (api r-200) "/" {:value 123}) => (fails-with 500))
+
+    (fact ":default"
+      (get* (api r-default) "/") => (has-body {:value "123"})
+      (get* (api r-default) "/" {:value 123}) => (fails-with 500))
+
+    (fact ":default"
+      (get* (api r-200-default) "/") => (has-body {:value "123"})
+      (get* (api r-200-default) "/" {:value 123}) => (fails-with 500))))
+
+(fact "custom coercion"
+
+  (fact "response coercion"
+    (let [ping-route (GET "/ping" []
+                       :return {:pong s/Str}
+                       (ok {:pong 123}))]
+
+      (fact "by default, applies response coercion"
+        (let [app (api
+                    ping-route)]
+          (get* app "/ping") => (fails-with 500)))
+
+      (fact "response-coercion can be disabled"
+        (fact "separately"
+          (let [app (api
+                      {:coercion mw/no-response-coercion}
+                      ping-route)]
+            (let [[status body] (get* app "/ping")]
+              status => 200
+              body => {:pong 123})))
+        (fact "all coercion"
+          (let [app (api
+                      {:coercion nil}
+                      ping-route)]
+            (let [[status body] (get* app "/ping")]
+              status => 200
+              body => {:pong 123}))))))
+
+  (fact "body coersion"
+    (let [beer-route (POST "/beer" []
+                       :body [body {:beers #{(s/enum "ipa" "apa")}}]
+                       (ok body))]
+
+      (fact "by default, applies body coercion (to set)"
+        (let [app (api
+                    beer-route)]
+          (let [[status body] (post* app "/beer" (json {:beers ["ipa" "apa" "ipa"]}))]
+            status => 200
+            body => {:beers ["ipa" "apa"]})))
+
+      (fact "body-coercion can be disabled"
+        (let [no-body-coercion (constantly (dissoc mw/default-coercion-matchers :body))
+              app (api
+                    {:coercion no-body-coercion}
+                    beer-route)]
+          (let [[status body] (post* app "/beer" (json {:beers ["ipa" "apa" "ipa"]}))]
+            status => 200
+            body => {:beers ["ipa" "apa" "ipa"]}))
+        (let [app (api
+                    {:coercion nil}
+                    beer-route)]
+          (let [[status body] (post* app "/beer" (json {:beers ["ipa" "apa" "ipa"]}))]
+            status => 200
+            body => {:beers ["ipa" "apa" "ipa"]})))
+
+      (fact "body-coercion can be changed"
+        (let [nop-body-coercion (constantly (assoc mw/default-coercion-matchers :body (constantly nil)))
+              app (api
+                    {:coercion nop-body-coercion}
+                    beer-route)]
+          (post* app "/beer" (json {:beers ["ipa" "apa" "ipa"]})) => (fails-with 400)))))
+
+  (fact "query coercion"
+    (let [query-route (GET "/query" []
+                        :query-params [i :- s/Int]
+                        (ok {:i i}))]
+
+      (fact "by default, applies query coercion (string->int)"
+        (let [app (api
+                    query-route)]
+          (let [[status body] (get* app "/query" {:i 10})]
+            status => 200
+            body => {:i 10})))
+
+      (fact "query-coercion can be disabled"
+        (let [no-query-coercion (constantly (dissoc mw/default-coercion-matchers :string))
+              app (api
+                    {:coercion no-query-coercion}
+                    query-route)]
+          (let [[status body] (get* app "/query" {:i 10})]
+            status => 200
+            body => {:i "10"})))
+
+      (fact "query-coercion can be changed"
+        (let [nop-query-coercion (constantly (assoc mw/default-coercion-matchers :string (constantly nil)))
+              app (api
+                    {:coercion nop-query-coercion}
+                    query-route)]
+          (get* app "/query" {:i 10}) => (fails-with 400)))))
+
+  (fact "route-specific coercion"
+    (let [app (api
+                (GET "/default" []
+                  :query-params [i :- s/Int]
+                  (ok {:i i}))
+                (GET "/disabled-coercion" []
+                  :coercion (constantly (assoc mw/default-coercion-matchers :string (constantly nil)))
+                  :query-params [i :- s/Int]
+                  (ok {:i i}))
+                (GET "/no-coercion" []
+                  :coercion (constantly nil)
+                  :query-params [i :- s/Int]
+                  (ok {:i i}))
+                (GET "/nil-coercion" []
+                  :coercion nil
+                  :query-params [i :- s/Int]
+                  (ok {:i i})))]
+
+      (fact "default coercion"
+        (let [[status body] (get* app "/default" {:i 10})]
+          status => 200
+          body => {:i 10}))
+
+      (fact "disabled coercion"
+        (get* app "/disabled-coercion" {:i 10}) => (fails-with 400))
+
+      (fact "no coercion"
+        (let [[status body] (get* app "/no-coercion" {:i 10})]
+          status => 200
+          body => {:i "10"})
+        (let [[status body] (get* app "/nil-coercion" {:i 10})]
+          status => 200
+          body => {:i "10"})))))
+
+(facts "apiless coercion"
+
+  (fact "use default-coercion-matchers by default"
+    (let [app (context "/api" []
+                :query-params [{y :- Long 0}]
+                (GET "/ping" []
+                  :query-params [x :- Long]
+                  (ok [x y])))]
+      (app {:request-method :get :uri "/api/ping" :query-params {}}) => throws
+      (app {:request-method :get :uri "/api/ping" :query-params {:x "abba"}}) => throws
+      (app {:request-method :get :uri "/api/ping" :query-params {:x "1"}}) => (contains {:body [1 0]})
+      (app {:request-method :get :uri "/api/ping" :query-params {:x "1", :y 2}}) => (contains {:body [1 2]})
+      (app {:request-method :get :uri "/api/ping" :query-params {:x "1", :y "abba"}}) => throws))
+
+  (fact "coercion can be overridden"
+    (let [app (context "/api" []
+                :query-params [{y :- Long 0}]
+                (GET "/ping" []
+                  :coercion (constantly nil)
+                  :query-params [x :- Long]
+                  (ok [x y])))]
+      (app {:request-method :get :uri "/api/ping" :query-params {}}) => throws
+      (app {:request-method :get :uri "/api/ping" :query-params {:x "abba"}}) => (contains {:body ["abba" 0]})
+      (app {:request-method :get :uri "/api/ping" :query-params {:x "1"}}) => (contains {:body ["1" 0]})
+      (app {:request-method :get :uri "/api/ping" :query-params {:x "1", :y 2}}) => (contains {:body ["1" 2]})
+      (app {:request-method :get :uri "/api/ping" :query-params {:x "1", :y "abba"}}) => throws))
+
+  (fact "context coercion is used for subroutes"
+    (let [app (context "/api" []
+                :coercion nil
+                (GET "/ping" []
+                  :query-params [x :- Long]
+                  (ok x)))]
+      (app {:request-method :get :uri "/api/ping" :query-params {:x "abba"}}) => (contains {:body "abba"}))))
diff --git a/test-suites/compojure1/test/compojure/api/common_test.clj b/test-suites/compojure1/test/compojure/api/common_test.clj
new file mode 100644
index 00000000..04a1a411
--- /dev/null
+++ b/test-suites/compojure1/test/compojure/api/common_test.clj
@@ -0,0 +1,28 @@
+(ns compojure.api.common-test
+  (:require [compojure.api.common :as common]
+            [midje.sweet :refer :all]))
+
+(fact "group-with"
+  (common/group-with pos? [1 -10 2 -4 -1 999]) => [[1 2 999] [-10 -4 -1]]
+  (common/group-with pos? [1 2 999]) => [[1 2 999] nil])
+
+(fact "extract-parameters"
+
+  (facts "expect body"
+    (common/extract-parameters [] true) => [{} nil]
+    (common/extract-parameters [{:a 1}] true) => [{} [{:a 1}]]
+    (common/extract-parameters [:a 1] true) => [{:a 1} nil]
+    (common/extract-parameters [{:a 1} {:b 2}] true) => [{:a 1} [{:b 2}]]
+    (common/extract-parameters [:a 1 {:b 2}] true) => [{:a 1} [{:b 2}]])
+
+  (facts "don't expect body"
+    (common/extract-parameters [] false) => [{} nil]
+    (common/extract-parameters [{:a 1}] false) => [{:a 1} nil]
+    (common/extract-parameters [:a 1] false) => [{:a 1} nil]
+    (common/extract-parameters [{:a 1} {:b 2}] false) => [{:a 1} [{:b 2}]]
+    (common/extract-parameters [:a 1 {:b 2}] false) => [{:a 1} [{:b 2}]]))
+
+(fact "merge-vector"
+  (common/merge-vector nil) => nil
+  (common/merge-vector [{:a 1}]) => {:a 1}
+  (common/merge-vector [{:a 1} {:b 2}]) => {:a 1 :b 2})
diff --git a/test-suites/compojure1/test/compojure/api/compojure_perf_test.clj b/test-suites/compojure1/test/compojure/api/compojure_perf_test.clj
new file mode 100644
index 00000000..06fa9475
--- /dev/null
+++ b/test-suites/compojure1/test/compojure/api/compojure_perf_test.clj
@@ -0,0 +1,126 @@
+(ns compojure.api.compojure-perf-test
+  (:require [compojure.core :as c]
+            [compojure.api.sweet :as s]
+            [criterium.core :as cc]
+            [ring.util.http-response :refer :all]))
+
+;;
+;; start repl with `lein perf repl`
+;; perf measured with the following setup:
+;;
+;; Model Name:            MacBook Pro
+;; Model Identifier:      MacBookPro11,3
+;; Processor Name:        Intel Core i7
+;; Processor Speed:       2,5 GHz
+;; Number of Processors:  1
+;; Total Number of Cores: 4
+;; L2 Cache (per Core):   256 KB
+;; L3 Cache:              6 MB
+;; Memory:                16 GB
+;;
+
+(defn title [s]
+  (println
+    (str "\n\u001B[35m"
+         (apply str (repeat (+ 6 (count s)) "#"))
+         "\n## " s " ##\n"
+         (apply str (repeat (+ 6 (count s)) "#"))
+         "\u001B[0m\n")))
+
+(defn compojure-bench []
+
+  (let [app (c/routes
+              (c/GET "/a/b/c/1" [] "ok")
+              (c/GET "/a/b/c/2" [] "ok")
+              (c/GET "/a/b/c/3" [] "ok")
+              (c/GET "/a/b/c/4" [] "ok")
+              (c/GET "/a/b/c/5" [] "ok"))
+
+        call #(app {:request-method :get :uri "/a/b/c/5"})]
+
+    (title "Compojure - GET flattened")
+    (assert (-> (call) :body (= "ok")))
+    (cc/quick-bench (call)))
+
+  ;; 3.8µs
+
+  (let [app (c/context "/a" []
+              (c/context "/b" []
+                (c/context "/c" []
+                  (c/GET "/1" [] "ok")
+                  (c/GET "/2" [] "ok")
+                  (c/GET "/3" [] "ok")
+                  (c/GET "/4" [] "ok")
+                  (c/GET "/5" [] "ok"))
+                (c/GET "/1" [] "ok")
+                (c/GET "/2" [] "ok")
+                (c/GET "/3" [] "ok")
+                (c/GET "/4" [] "ok")
+                (c/GET "/5" [] "ok"))
+              (c/GET "/1" [] "ok")
+              (c/GET "/2" [] "ok")
+              (c/GET "/3" [] "ok")
+              (c/GET "/4" [] "ok")
+              (c/GET "/5" [] "ok"))
+
+        call #(app {:request-method :get :uri "/a/b/c/5"})]
+
+    (title "Compojure - GET with context")
+    (assert (-> (call) :body (= "ok")))
+    (cc/quick-bench (call)))
+
+  ;; 15.9µs
+
+  )
+
+(defn compojure-api-bench []
+
+  (let [app (s/routes
+              (s/GET "/a/b/c/1" [] "ok")
+              (s/GET "/a/b/c/2" [] "ok")
+              (s/GET "/a/b/c/3" [] "ok")
+              (s/GET "/a/b/c/4" [] "ok")
+              (s/GET "/a/b/c/5" [] "ok"))
+
+        call #(app {:request-method :get :uri "/a/b/c/5"})]
+
+    (title "Compojure API - GET flattened")
+    (assert (-> (call) :body (= "ok")))
+    (cc/quick-bench (call)))
+
+  ;; 3.8µs
+
+  (let [app (s/context "/a" []
+              (s/context "/b" []
+                (s/context "/c" []
+                  (s/GET "/1" [] "ok")
+                  (s/GET "/2" [] "ok")
+                  (s/GET "/3" [] "ok")
+                  (s/GET "/4" [] "ok")
+                  (s/GET "/5" [] "ok"))
+                (s/GET "/1" [] "ok")
+                (s/GET "/2" [] "ok")
+                (s/GET "/3" [] "ok")
+                (s/GET "/4" [] "ok")
+                (s/GET "/5" [] "ok"))
+              (s/GET "/1" [] "ok")
+              (s/GET "/2" [] "ok")
+              (s/GET "/3" [] "ok")
+              (s/GET "/4" [] "ok")
+              (s/GET "/5" [] "ok"))
+
+        call #(app {:request-method :get :uri "/a/b/c/5"})]
+
+    (title "Compojure API - GET with context")
+    (assert (-> (call) :body (= "ok")))
+    (cc/quick-bench (call)))
+
+  ;; 20.0µs
+  )
+
+(defn bench []
+  (compojure-bench)
+  (compojure-api-bench))
+
+(comment
+  (bench))
diff --git a/test-suites/compojure1/test/compojure/api/exception_test.clj b/test-suites/compojure1/test/compojure/api/exception_test.clj
new file mode 100644
index 00000000..1432ddc4
--- /dev/null
+++ b/test-suites/compojure1/test/compojure/api/exception_test.clj
@@ -0,0 +1,14 @@
+(ns compojure.api.exception-test
+  (:require [compojure.api.exception :refer :all]
+            [midje.sweet :refer :all]
+            [schema.core :as s])
+  (:import [schema.utils ValidationError NamedError]))
+
+(fact "stringify-error"
+  (fact "ValidationError"
+    (class (s/check s/Int "foo")) => ValidationError
+    (stringify-error (s/check s/Int "foo")) => "(not (integer? \"foo\"))"
+    (stringify-error (s/check {:foo s/Int} {:foo "foo"})) => {:foo "(not (integer? \"foo\"))"})
+  (fact "NamedError"
+    (class (s/check (s/named s/Int "name") "foo")) => NamedError
+    (stringify-error (s/check (s/named s/Int "name") "foo")) => "(named (not (integer? \"foo\")) \"name\")"))
diff --git a/test-suites/compojure1/test/compojure/api/integration_test.clj b/test-suites/compojure1/test/compojure/api/integration_test.clj
new file mode 100644
index 00000000..1c907ded
--- /dev/null
+++ b/test-suites/compojure1/test/compojure/api/integration_test.clj
@@ -0,0 +1,1511 @@
+(ns compojure.api.integration-test
+  (:require [compojure.api.sweet :refer :all]
+            [compojure.api.test-utils :refer :all]
+            [compojure.api.exception :as ex]
+            [compojure.api.swagger :as swagger]
+            [midje.sweet :refer :all]
+            [ring.util.http-response :refer :all]
+            [schema.core :as s]
+            [ring.swagger.core :as rsc]
+            [ring.util.http-status :as status]
+            [compojure.api.middleware :as mw]
+            [ring.swagger.middleware :as rsm]
+            [compojure.api.validator :as validator]
+            [compojure.api.routes :as routes]
+
+            [ring.middleware.format-response :as format-response]
+            [cheshire.core :as json]))
+
+;;
+;; Data
+;;
+
+(s/defschema User {:id Long
+                   :name String})
+
+(def pertti {:id 1 :name "Pertti"})
+
+(def invalid-user {:id 1 :name "Jorma" :age 50})
+
+; Headers contain extra keys, so make the schema open
+(s/defschema UserHeaders
+  (assoc User
+    s/Keyword s/Any))
+
+;;
+;; Middleware setup
+;;
+
+(def mw* "mw")
+
+(defn middleware*
+  "This middleware appends given value or 1 to a header in request and response."
+  ([handler] (middleware* handler 1))
+  ([handler value]
+   (fn [request]
+     (let [append #(str % value)
+           request (update-in request [:headers mw*] append)
+           response (handler request)]
+       (update-in response [:headers mw*] append)))))
+
+(defn constant-middleware
+  "This middleware rewrites all responses with a constant response."
+  [_ res]
+  (constantly res))
+
+(defn reply-mw*
+  "Handler which replies with response where a header contains copy
+   of the headers value from request and 7"
+  [request]
+  (-> (ok "true")
+      (header mw* (str (get-in request [:headers mw*]) "/"))))
+
+(defn middleware-x
+  "If request has query-param x, presume it's a integer and multiply it by two
+   before passing request to next handler."
+  [handler]
+  (fn [req]
+    (handler (update-in req [:query-params "x"] #(* (Integer. %) 2)))))
+
+(defn custom-validation-error-handler [ex data request]
+  (let [error-body {:custom-error (:uri request)}]
+    (case (:type data)
+      ::ex/response-validation (not-implemented error-body)
+      (bad-request error-body))))
+
+(defn custom-exception-handler [^Exception ex data request]
+  (ok {:custom-exception (str ex)}))
+
+(defn custom-error-handler [ex data request]
+  (ok {:custom-error (:data data)}))
+
+;;
+;; Facts
+;;
+
+(facts "core routes"
+
+  (fact "keyword options"
+    (let [route (GET "/ping" []
+                  :return String
+                  (ok "kikka"))]
+      (route {:request-method :get :uri "/ping"}) => (contains {:body "kikka"})))
+
+  (fact "map options"
+    (let [route (GET "/ping" []
+                  {:return String}
+                  (ok "kikka"))]
+      (route {:request-method :get :uri "/ping"}) => (contains {:body "kikka"})))
+
+  (fact "map return"
+    (let [route (GET "/ping" []
+                  {:body "kikka"})]
+      (route {:request-method :get :uri "/ping"}) => (contains {:body "kikka"}))))
+
+(facts "middleware ordering"
+  (let [app (api
+              (middleware [middleware* [middleware* 2]]
+                (context "/middlewares" []
+                  :middleware [(fn [handler] (middleware* handler 3)) [middleware* 4]]
+                  (GET "/simple" req (reply-mw* req))
+                  (middleware [#(middleware* % 5) [middleware* 6]]
+                    (GET "/nested" req (reply-mw* req))
+                    (GET "/nested-declared" req
+                      :middleware [(fn [handler] (middleware* handler 7)) [middleware* 8]]
+                      (reply-mw* req))))))]
+
+    (fact "are applied left-to-right"
+      (let [[status _ headers] (get* app "/middlewares/simple" {})]
+        status => 200
+        (get headers mw*) => "1234/4321"))
+
+    (fact "are applied left-to-right closest one first"
+      (let [[status _ headers] (get* app "/middlewares/nested" {})]
+        status => 200
+        (get headers mw*) => "123456/654321"))
+
+    (fact "are applied left-to-right for both nested & declared closest one first"
+      (let [[status _ headers] (get* app "/middlewares/nested-declared" {})]
+        status => 200
+        (get headers mw*) => "12345678/87654321"))))
+
+(facts "middleware - multiple routes"
+  (let [app (api
+              (GET "/first" []
+                (ok {:value "first"}))
+              (GET "/second" []
+                :middleware [[constant-middleware (ok {:value "foo"})]]
+                (ok {:value "second"}))
+              (GET "/third" []
+                (ok {:value "third"})))]
+    (fact "first returns first"
+      (let [[status body] (get* app "/first" {})]
+        status => 200
+        body => {:value "first"}))
+    (fact "second returns foo"
+      (let [[status body] (get* app "/second" {})]
+        status => 200
+        body => {:value "foo"}))
+    (fact "third returns third"
+      (let [[status body] (get* app "/third" {})]
+        status => 200
+        body => {:value "third"}))))
+
+(facts "middleware - editing request"
+  (let [app (api
+              (GET "/first" []
+                :query-params [x :- Long]
+                :middleware [middleware-x]
+                (ok {:value x})))]
+    (fact "middleware edits the parameter before route body"
+      (let [[status body] (get* app "/first?x=5" {})]
+        status => 200
+        body => {:value 10}))))
+
+(fact ":body, :query, :headers and :return"
+  (let [app (api
+              (context "/models" []
+                (GET "/pertti" []
+                  :return User
+                  (ok pertti))
+                (GET "/user" []
+                  :return User
+                  :query [user User]
+                  (ok user))
+                (GET "/invalid-user" []
+                  :return User
+                  (ok invalid-user))
+                (GET "/not-validated" []
+                  (ok invalid-user))
+                (POST "/user" []
+                  :return User
+                  :body [user User]
+                  (ok user))
+                (POST "/user_list" []
+                  :return [User]
+                  :body [users [User]]
+                  (ok users))
+                (POST "/user_set" []
+                  :return #{User}
+                  :body [users #{User}]
+                  (ok users))
+                (POST "/user_headers" []
+                  :return User
+                  :headers [user UserHeaders]
+                  (ok (select-keys user [:id :name])))
+                (POST "/user_legacy" {user :body-params}
+                  :return User
+                  (ok user))))]
+
+    (fact "GET"
+      (let [[status body] (get* app "/models/pertti")]
+        status => 200
+        body => pertti))
+
+    (fact "GET with smart destructuring"
+      (let [[status body] (get* app "/models/user" pertti)]
+        status => 200
+        body => pertti))
+
+    (fact "POST with smart destructuring"
+      (let [[status body] (post* app "/models/user" (json pertti))]
+        status => 200
+        body => pertti))
+
+    (fact "POST with smart destructuring - lists"
+      (let [[status body] (post* app "/models/user_list" (json [pertti]))]
+        status => 200
+        body => [pertti]))
+
+    (fact "POST with smart destructuring - sets"
+      (let [[status body] (post* app "/models/user_set" (json #{pertti}))]
+        status => 200
+        body => [pertti]))
+
+    (fact "POST with compojure destructuring"
+      (let [[status body] (post* app "/models/user_legacy" (json pertti))]
+        status => 200
+        body => pertti))
+
+    (fact "POST with smart destructuring - headers"
+      (let [[status body] (headers-post* app "/models/user_headers" pertti)]
+        status => 200
+        body => pertti))
+
+    (fact "Validation of returned data"
+      (let [[status] (get* app "/models/invalid-user")]
+        status => 500))
+
+    (fact "Routes without a :return parameter aren't validated"
+      (let [[status body] (get* app "/models/not-validated")]
+        status => 200
+        body => invalid-user))
+
+    (fact "Invalid json in body causes 400 with error message in json"
+      (let [[status body] (post* app "/models/user" "{INVALID}")]
+        status => 400
+        (:message body) => (contains "Unexpected character")))))
+
+(fact ":responses"
+  (fact "normal cases"
+    (let [app (api
+                (swagger-routes)
+                (GET "/lotto/:x" []
+                  :path-params [x :- Long]
+                  :responses {403 {:schema [String]}
+                              440 {:schema [String]}}
+                  :return [Long]
+                  (case x
+                    1 (ok [1])
+                    2 (ok ["two"])
+                    3 (forbidden ["error"])
+                    4 (forbidden [1])
+                    (not-found {:message "not-found"}))))]
+
+      (fact "return case"
+        (let [[status body] (get* app "/lotto/1")]
+          status => 200
+          body => [1]))
+
+      (fact "return case, non-matching model"
+        (let [[status body] (get* app "/lotto/2")]
+          status => 500
+          body => (contains {:errors vector?})))
+
+      (fact "error case"
+        (let [[status body] (get* app "/lotto/3")]
+          status => 403
+          body => ["error"]))
+
+      (fact "error case, non-matching model"
+        (let [[status body] (get* app "/lotto/4")]
+          status => 500
+          body => (contains {:errors vector?})))
+
+      (fact "returning non-predefined http-status code works"
+        (let [[status body] (get* app "/lotto/5")]
+          body => {:message "not-found"}
+          status => 404))
+
+      (fact "swagger-docs for multiple returns"
+        (-> app get-spec :paths vals first :get :responses keys set))))
+
+  (fact ":responses 200 and :return"
+    (let [app (api
+                (GET "/lotto/:x" []
+                  :path-params [x :- Long]
+                  :return {:return String}
+                  :responses {200 {:schema {:value String}}}
+                  (case x
+                    1 (ok {:return "ok"})
+                    2 (ok {:value "ok"}))))]
+
+      (fact "return case"
+        (let [[status body] (get* app "/lotto/1")]
+          status => 500
+          body => (contains {:errors {:return "disallowed-key"
+                                      :value "missing-required-key"}})))
+
+      (fact "return case"
+        (let [[status body] (get* app "/lotto/2")]
+          status => 200
+          body => {:value "ok"}))))
+
+  (fact ":responses 200 and :return - other way around"
+    (let [app (api
+                (GET "/lotto/:x" []
+                  :path-params [x :- Long]
+                  :responses {200 {:schema {:value String}}}
+                  :return {:return String}
+                  (case x
+                    1 (ok {:return "ok"})
+                    2 (ok {:value "ok"}))))]
+
+      (fact "return case"
+        (let [[status body] (get* app "/lotto/1")]
+          status => 200
+          body => {:return "ok"}))
+
+      (fact "return case"
+        (let [[status body] (get* app "/lotto/2")]
+          status => 500
+          body => (contains {:errors {:return "missing-required-key"
+                                      :value "disallowed-key"}}))))))
+
+(fact ":query-params, :path-params, :header-params , :body-params and :form-params"
+  (let [app (api
+              (context "/smart" []
+                (GET "/plus" []
+                  :query-params [x :- Long y :- Long]
+                  (ok {:total (+ x y)}))
+                (GET "/multiply/:x/:y" []
+                  :path-params [x :- Long y :- Long]
+                  (ok {:total (* x y)}))
+                (GET "/power" []
+                  :header-params [x :- Long y :- Long]
+                  (ok {:total (long (Math/pow x y))}))
+                (POST "/minus" []
+                  :body-params [x :- Long {y :- Long 1}]
+                  (ok {:total (- x y)}))
+                (POST "/divide" []
+                  :form-params [x :- Long y :- Long]
+                  (ok {:total (/ x y)}))))]
+
+    (fact "query-parameters"
+      (let [[status body] (get* app "/smart/plus" {:x 2 :y 3})]
+        status => 200
+        body => {:total 5}))
+
+    (fact "path-parameters"
+      (let [[status body] (get* app "/smart/multiply/2/3")]
+        status => 200
+        body => {:total 6}))
+
+    (fact "header-parameters"
+      (let [[status body] (get* app "/smart/power" {} {:x 2 :y 3})]
+        status => 200
+        body => {:total 8}))
+
+    (fact "form-parameters"
+      (let [[status body] (form-post* app "/smart/divide" {:x 6 :y 3})]
+        status => 200
+        body => {:total 2}))
+
+    (fact "body-parameters"
+      (let [[status body] (post* app "/smart/minus" (json {:x 2 :y 3}))]
+        status => 200
+        body => {:total -1}))
+
+    (fact "default parameters"
+      (let [[status body] (post* app "/smart/minus" (json {:x 2}))]
+        status => 200
+        body => {:total 1}))))
+
+(fact "primitive support"
+  (let [app (api
+              {:swagger {:spec "/swagger.json"}}
+              (context "/primitives" []
+                (GET "/return-long" []
+                  :return Long
+                  (ok 1))
+                (GET "/long" []
+                  (ok 1))
+                (GET "/return-string" []
+                  :return String
+                  (ok "kikka"))
+                (POST "/arrays" []
+                  :return [Long]
+                  :body [longs [Long]]
+                  (ok longs))))]
+
+    (fact "when :return is set, longs can be returned"
+      (let [[status body] (raw-get* app "/primitives/return-long")]
+        status => 200
+        body => "1"))
+
+    (fact "when :return is not set, longs won't be encoded"
+      (let [[status body] (raw-get* app "/primitives/long")]
+        status => 200
+        body => number?))
+
+    (fact "when :return is set, raw strings can be returned"
+      (let [[status body] (raw-get* app "/primitives/return-string")]
+        status => 200
+        body => "\"kikka\""))
+
+    (fact "primitive arrays work"
+      (let [[status body] (raw-post* app "/primitives/arrays" (json/generate-string [1 2 3]))]
+        status => 200
+        body => "[1,2,3]"))
+
+    (fact "swagger-spec is valid"
+      (validator/validate app))
+
+    (fact "primitive array swagger-docs are good"
+
+      (-> app get-spec :paths (get "/primitives/arrays") :post :parameters)
+      => [{:description ""
+           :in "body"
+           :name ""
+           :required true
+           :schema {:items {:format "int64"
+                            :type "integer"}
+                    :type "array"}}]
+
+      (-> app get-spec :paths (get "/primitives/arrays") :post :responses :200 :schema)
+      => {:items {:format "int64",
+                  :type "integer"},
+          :type "array"})))
+
+(fact "compojure destructuring support"
+  (let [app (api
+              (context "/destructuring" []
+                (GET "/regular" {{:keys [a]} :params}
+                  (ok {:a a
+                       :b (-> +compojure-api-request+ :params :b)}))
+                (GET "/regular2" {:as req}
+                  (ok {:a (-> req :params :a)
+                       :b (-> +compojure-api-request+ :params :b)}))
+                (GET "/vector" [a]
+                  (ok {:a a
+                       :b (-> +compojure-api-request+ :params :b)}))
+                (GET "/vector2" [:as req]
+                  (ok {:a (-> req :params :a)
+                       :b (-> +compojure-api-request+ :params :b)}))
+                (GET "/symbol" req
+                  (ok {:a (-> req :params :a)
+                       :b (-> +compojure-api-request+ :params :b)}))
+                (GET "/integrated" [a] :query-params [b]
+                  (ok {:a a
+                       :b b}))))]
+
+    (doseq [uri ["regular" "regular2" "vector" "vector2" "symbol" "integrated"]]
+      (fact {:midje/description uri}
+        (let [[status body] (get* app (str "/destructuring/" uri) {:a "a" :b "b"})]
+          status => 200
+          body => {:a "a" :b "b"})))))
+
+(fact "counting execution times, issue #19"
+  (let [execution-times (atom 0)
+        app (api
+              (GET "/user" []
+                :return User
+                :query [user User]
+                (swap! execution-times inc)
+                (ok user)))]
+
+    (fact "body is executed one"
+      @execution-times => 0
+      (let [[status body] (get* app "/user" pertti)]
+        status => 200
+        body => pertti)
+      @execution-times => 1)))
+
+(fact "swagger-docs"
+  (let [app (api
+              {:format {:formats [:json-kw :edn :UNKNOWN]}}
+              (swagger-routes)
+              (GET "/user" []
+                (continue)))]
+
+    (fact "api-listing shows produces & consumes for known types"
+      (get-spec app) => {:swagger "2.0"
+                         :info {:title "Swagger API"
+                                :version "0.0.1"}
+                         :basePath "/"
+                         :consumes ["application/json" "application/edn"]
+                         :produces ["application/json" "application/edn"]
+                         :definitions {}
+                         :paths {"/user" {:get {:responses {:default {:description ""}}}}}}))
+
+  (fact "swagger-routes"
+
+    (fact "with defaults"
+      (let [app (api (swagger-routes))]
+
+        (fact "api-docs are mounted to /"
+          (let [[status body] (raw-get* app "/")]
+            status => 200
+            body => #"<title>Swagger UI</title>"))
+
+        (fact "spec is mounted to /swagger.json"
+          (let [[status body] (get* app "/swagger.json")]
+            status => 200
+            body => (contains {:swagger "2.0"})))))
+
+    (fact "with partial overridden values"
+      (let [app (api (swagger-routes {:ui "/api-docs"
+                                      :data {:info {:title "Kikka"}
+                                             :paths {"/ping" {:get {}}}}}))]
+
+        (fact "api-docs are mounted"
+          (let [[status body] (raw-get* app "/api-docs")]
+            status => 200
+            body => #"<title>Swagger UI</title>"))
+
+        (fact "spec is mounted to /swagger.json"
+          (let [[status body] (get* app "/swagger.json")]
+            status => 200
+            body => (contains
+                      {:swagger "2.0"
+                       :info (contains
+                               {:title "Kikka"})
+                       :paths (contains
+                                {(keyword "/ping") anything})}))))))
+
+  (fact "swagger via api-options"
+
+    (fact "with defaults"
+      (let [app (api)]
+
+        (fact "api-docs are not mounted"
+          (let [[status body] (raw-get* app "/")]
+            status => nil))
+
+        (fact "spec is not mounted"
+          (let [[status body] (get* app "/swagger.json")]
+            status => nil))))
+
+    (fact "with spec"
+      (let [app (api {:swagger {:spec "/swagger.json"}})]
+
+        (fact "api-docs are not mounted"
+          (let [[status body] (raw-get* app "/")]
+            status => nil))
+
+        (fact "spec is mounted to /swagger.json"
+          (let [[status body] (get* app "/swagger.json")]
+            status => 200
+            body => (contains {:swagger "2.0"}))))))
+
+  (fact "with ui"
+    (let [app (api {:swagger {:ui "/api-docs"}})]
+
+      (fact "api-docs are mounted"
+        (let [[status body] (raw-get* app "/api-docs")]
+          status => 200
+          body => #"<title>Swagger UI</title>"))
+
+      (fact "spec is not mounted"
+        (let [[status body] (get* app "/swagger.json")]
+          status => nil))))
+
+  (fact "with ui and spec"
+    (let [app (api {:swagger {:spec "/swagger.json", :ui "/api-docs"}})]
+
+      (fact "api-docs are mounted"
+        (let [[status body] (raw-get* app "/api-docs")]
+          status => 200
+          body => #"<title>Swagger UI</title>"))
+
+      (fact "spec is mounted to /swagger.json"
+        (let [[status body] (get* app "/swagger.json")]
+          status => 200
+          body => (contains {:swagger "2.0"}))))))
+
+(facts "swagger-docs with anonymous Return and Body models"
+  (let [app (api
+              (swagger-routes)
+              (POST "/echo" []
+                :return (s/either {:a String})
+                :body [_ (s/maybe {:a String})]
+                identity))]
+
+    (fact "api-docs"
+      (let [spec (get-spec app)]
+
+        (let [operation (some-> spec :paths vals first :post)
+              body-ref (some-> operation :parameters first :schema :$ref)
+              return-ref (get-in operation [:responses :200 :schema :$ref])]
+
+          (fact "generated body-param is found in Definitions"
+            (find-definition spec body-ref) => truthy)
+
+          (fact "generated return-param is found in Definitions"
+            return-ref => truthy
+            (find-definition spec body-ref) => truthy))))))
+
+(def Boundary
+  {:type (s/enum "MultiPolygon" "Polygon" "MultiPoint" "Point")
+   :coordinates [s/Any]})
+
+(def ReturnValue
+  {:boundary (s/maybe Boundary)})
+
+(facts "https://github.com/metosin/compojure-api/issues/53"
+  (let [app (api
+              (swagger-routes)
+              (POST "/" []
+                :return ReturnValue
+                :body [_ Boundary]
+                identity))]
+
+    (fact "api-docs"
+      (let [spec (get-spec app)]
+
+        (let [operation (some-> spec :paths vals first :post)
+              body-ref (some-> operation :parameters first :schema :$ref)
+              return-ref (get-in operation [:responses :200 :schema :$ref])]
+
+          (fact "generated body-param is found in Definitions"
+            (find-definition spec body-ref) => truthy)
+
+          (fact "generated return-param is found in Definitions"
+            return-ref => truthy
+            (find-definition spec body-ref) => truthy))))))
+
+(s/defschema Urho {:kaleva {:kekkonen {s/Keyword s/Any}}})
+(s/defschema Olipa {:kerran {:avaruus {s/Keyword s/Any}}})
+
+; https://github.com/metosin/compojure-api/issues/94
+(facts "preserves deeply nested schema names"
+  (let [app (api
+              (swagger-routes)
+              (POST "/" []
+                :return Urho
+                :body [_ Olipa]
+                identity))]
+
+    (fact "api-docs"
+      (let [spec (get-spec app)]
+
+        (fact "nested models are discovered correctly"
+          (-> spec :definitions keys set)
+
+          => #{:Urho :UrhoKaleva :UrhoKalevaKekkonen
+               :Olipa :OlipaKerran :OlipaKerranAvaruus})))))
+
+(fact "swagger-docs works with the :middleware"
+  (let [app (api
+              (swagger-routes)
+              (GET "/middleware" []
+                :query-params [x :- String]
+                :middleware [[constant-middleware (ok 1)]]
+                (ok 2)))]
+
+    (fact "api-docs"
+      (-> app get-spec :paths vals first)
+      => {:get {:parameters [{:description ""
+                              :in "query"
+                              :name "x"
+                              :required true
+                              :type "string"}]
+                :responses {:default {:description ""}}}})))
+
+(fact "sub-context paths"
+  (let [response {:ping "pong"}
+        ok (ok response)
+        ok? (fn [[status body]]
+              (and (= status 200)
+                   (= body response)))
+        not-ok? (comp not ok?)
+        app (api
+              (swagger-routes {:ui nil})
+              (GET "/" [] ok)
+              (GET "/a" [] ok)
+              (context "/b" []
+                (context "/b1" []
+                  (GET "/" [] ok))
+                (context "/" []
+                  (GET "/" [] ok)
+                  (GET "/b2" [] ok))))]
+
+    (fact "valid routes"
+      (get* app "/") => ok?
+      (get* app "/a") => ok?
+      (get* app "/b/b1") => ok?
+      (get* app "/b") => ok?
+      (get* app "/b/b2") => ok?)
+
+    (fact "undocumented compojure easter eggs"
+      (get* app "/b/b1/") => ok?
+      (get* app "/b/") => ok?
+      (fact "this is fixed in compojure 1.5.1"
+        (get* app "/b//") =not=> ok?))
+
+    (fact "swagger-docs have trailing slashes removed"
+      (->> app get-spec :paths keys)
+      => ["/" "/a" "/b/b1" "/b" "/b/b2"])))
+
+(fact "formats supported by ring-middleware-format"
+  (let [app (api
+              (POST "/echo" []
+                :body-params [foo :- String]
+                (ok {:foo foo})))]
+
+    (tabular
+      (facts
+        (fact {:midje/description (str ?content-type " to json")}
+          (let [[status body]
+                (raw-post* app "/echo" ?body ?content-type {:accept "application/json"})]
+            status => 200
+            body => "{\"foo\":\"bar\"}"))
+        (fact {:midje/description (str "json to " ?content-type)}
+          (let [[status body]
+                (raw-post* app "/echo" "{\"foo\":\"bar\"}" "application/json" {:accept ?content-type})]
+            status => 200
+            body => ?body)))
+
+      ?content-type ?body
+      "application/json" "{\"foo\":\"bar\"}"
+      "application/x-yaml" "{foo: bar}\n"
+      "application/edn" "{:foo \"bar\"}"
+      "application/transit+json" "[\"^ \",\"~:foo\",\"bar\"]")))
+
+(fact "multiple routes in context"
+  (let [app (api
+              (context "/foo" []
+                (GET "/bar" [] (ok ["bar"]))
+                (GET "/baz" [] (ok ["baz"]))))]
+
+    (fact "first route works"
+      (let [[status body] (get* app "/foo/bar")]
+        status => 200
+        body => ["bar"]))
+    (fact "second route works"
+      (let [[status body] (get* app "/foo/baz")]
+        status => 200
+        body => ["baz"]))))
+
+(require '[compojure.api.test-domain :refer [Pizza burger-routes]])
+
+(fact "external deep schemas"
+  (let [app (api
+              (swagger-routes)
+              burger-routes
+              (POST "/pizza" []
+                :return Pizza
+                :body [body Pizza]
+                (ok body)))]
+
+    (fact "direct route with nested named schema works when called"
+      (let [pizza {:toppings [{:name "cheese"}]}
+            [status body] (post* app "/pizza" (json pizza))]
+        status => 200
+        body => pizza))
+
+    (fact "defroute*'d route with nested named schema works when called"
+      (let [burger {:ingredients [{:name "beef"}, {:name "egg"}]}
+            [status body] (post* app "/burger" (json burger))]
+        status => 200
+        body => burger))
+
+    (fact "generates correct swagger-spec"
+      (-> app get-spec :definitions keys set) => #{:Topping :Pizza :Burger :Beef})))
+
+(fact "multiple routes with same path & method in same file"
+  (let [app (api
+              (swagger-routes)
+              (GET "/ping" []
+                :summary "active-ping"
+                (ok {:ping "active"}))
+              (GET "/ping" []
+                :summary "passive-ping"
+                (ok {:ping "passive"})))]
+
+    (fact "first route matches with Compojure"
+      (let [[status body] (get* app "/ping" {})]
+        status => 200
+        body => {:ping "active"}))
+
+    (fact "generates correct swagger-spec"
+      (-> app get-spec :paths vals first :get :summary) => "active-ping")))
+
+(fact "multiple routes with same path & method over context"
+  (let [app (api
+              (swagger-routes)
+              (context "/api" []
+                (context "/ipa" []
+                  (GET "/ping" []
+                    :summary "active-ping"
+                    (ok {:ping "active"}))))
+              (context "/api" []
+                (context "/ipa" []
+                  (GET "/ping" []
+                    :summary "passive-ping"
+                    (ok {:ping "passive"})))))]
+
+    (fact "first route matches with Compojure"
+      (let [[status body] (get* app "/api/ipa/ping" {})]
+        status => 200
+        body => {:ping "active"}))
+
+    (fact "generates correct swagger-spec"
+      (-> app get-spec :paths vals first :get :summary) => "active-ping")))
+
+(fact "multiple routes with same overall path (with different path sniplets & method over context"
+  (let [app (api
+              (swagger-routes)
+              (context "/api/ipa" []
+                (GET "/ping" []
+                  :summary "active-ping"
+                  (ok {:ping "active"})))
+              (context "/api" []
+                (context "/ipa" []
+                  (GET "/ping" []
+                    :summary "passive-ping"
+                    (ok {:ping "passive"})))))]
+
+    (fact "first route matches with Compojure"
+      (let [[status body] (get* app "/api/ipa/ping" {})]
+        status => 200
+        body => {:ping "active"}))
+
+    (fact "generates correct swagger-spec"
+      (-> app get-spec :paths vals first :get :summary) => "active-ping")))
+
+; https://github.com/metosin/compojure-api/issues/98
+; https://github.com/metosin/compojure-api/issues/134
+(fact "basePath"
+  (let [app (api (swagger-routes))]
+
+    (fact "no context"
+      (-> app get-spec :basePath) => "/")
+
+    (fact "app-servers with given context"
+      (against-background (rsc/context anything) => "/v2")
+      (-> app get-spec :basePath) => "/v2"))
+
+  (let [app (api (swagger-routes {:data {:basePath "/serve/from/here"}}))]
+    (fact "override it"
+      (-> app get-spec :basePath) => "/serve/from/here"))
+
+  (let [app (api (swagger-routes {:data {:basePath "/"}}))]
+    (fact "can set it to the default"
+      (-> app get-spec :basePath) => "/")))
+
+(fact "multiple different models with same name"
+
+  (fact "schemas with same regexps are not equal"
+    {:d #"\D"} =not=> {:d #"\D"})
+
+  (fact "api-spec with 2 schemas with non-equal contents"
+    (let [app (api
+                (swagger-routes)
+                (GET "/" []
+                  :responses {200 {:schema (s/schema-with-name {:a {:d #"\D"}} "Kikka")}
+                              201 {:schema (s/schema-with-name {:a {:d #"\D"}} "Kikka")}}
+                  identity))]
+      (fact "api spec doesn't fail (#102)"
+        (get-spec app) => anything))))
+
+(def over-the-hills-and-far-away
+  (POST "/" []
+    :body-params [a :- s/Str]
+    identity))
+
+(fact "anonymous body models over defined routes"
+  (let [app (api
+              (swagger-routes)
+              over-the-hills-and-far-away)]
+    (fact "generated model doesn't have namespaced keys"
+      (-> app get-spec :definitions vals first :properties keys first) => :a)))
+
+(def foo
+  (GET "/foo" []
+    (let [foo {:foo "bar"}]
+      (ok foo))))
+
+(fact "defroutes with local symbol usage with same name (#123)"
+  (let [app (api
+              foo)]
+    (let [[status body] (get* app "/foo")]
+      status => 200
+      body => {:foo "bar"})))
+
+(def response-descriptions-routes
+  (GET "/x" []
+    :responses {500 {:schema {:code String}
+                     :description "Horror"}}
+    identity))
+
+(fact "response descriptions"
+  (let [app (api
+              (swagger-routes)
+              response-descriptions-routes)]
+    (-> app get-spec :paths vals first :get :responses :500 :description) => "Horror"))
+
+(fact "exceptions options with custom validation error handler"
+  (let [app (api
+              {:exceptions {:handlers {::ex/request-validation custom-validation-error-handler
+                                       ::ex/request-parsing custom-validation-error-handler
+                                       ::ex/response-validation custom-validation-error-handler}}}
+              (swagger-routes)
+              (POST "/get-long" []
+                :body [body {:x Long}]
+                :return Long
+                (case (:x body)
+                  1 (ok 1)
+                  (ok "not a number"))))]
+
+    (fact "return case, valid request & valid model"
+      (let [[status body] (post* app "/get-long" "{\"x\": 1}")]
+        status => 200
+        body => 1))
+
+    (fact "return case, not schema valid request"
+      (let [[status body] (post* app "/get-long" "{\"x\": \"1\"}")]
+        status => 400
+        body => (contains {:custom-error "/get-long"})))
+
+    (fact "return case, invalid json request"
+      (let [[status body] (post* app "/get-long" "{x: 1}")]
+        status => 400
+        body => (contains {:custom-error "/get-long"})))
+
+    (fact "return case, valid request & invalid model"
+      (let [[status body] (post* app "/get-long" "{\"x\": 2}")]
+        status => 501
+        body => (contains {:custom-error "/get-long"})))))
+
+(fact "exceptions options with custom exception and error handler"
+  (let [app (api
+              {:exceptions {:handlers {::ex/default custom-exception-handler
+                                       ::custom-error custom-error-handler}}}
+              (swagger-routes)
+              (GET "/some-exception" []
+                (throw (new RuntimeException)))
+              (GET "/some-error" []
+                (throw (ex-info "some ex info" {:data "some error" :type ::some-error})))
+              (GET "/specific-error" []
+                (throw (ex-info "my ex info" {:data "my error" :type ::custom-error}))))]
+
+    (fact "uses default exception handler for unknown exceptions"
+      (let [[status body] (get* app "/some-exception")]
+        status => 200
+        body => {:custom-exception "java.lang.RuntimeException"}))
+
+    (fact "uses default exception handler for unknown errors"
+      (let [[status body] (get* app "/some-error")]
+        status => 200
+        (:custom-exception body) => (contains ":data \"some error\"")))
+
+    (fact "uses specific error handler for ::custom-errors"
+      (let [[status body] (get* app "/specific-error")]
+        body => {:custom-error "my error"}))))
+
+(fact "exception handling can be disabled"
+  (let [app (api
+              {:exceptions nil}
+              (GET "/throw" []
+                (throw (new RuntimeException))))]
+    (get* app "/throw") => throws))
+
+(defn old-ex-handler [e]
+  {:status 500
+   :body {:type "unknown-exception"
+          :class (.getName (.getClass e))}})
+
+(fact "Deprecated options"
+  (facts "Old options throw assertion error"
+    (api {:validation-errors {:error-handler identity}} nil) => (throws AssertionError)
+    (api {:validation-errors {:catch-core-errors? true}} nil) => (throws AssertionError)
+    (api {:exceptions {:exception-handler identity}} nil) => (throws AssertionError))
+  (facts "Old handler functions work, with a warning"
+    (let [app (api
+                {:exceptions {:handlers {::ex/default old-ex-handler}}}
+                (GET "/" []
+                  (throw (RuntimeException.))))]
+      (with-out-str
+        (let [[status body] (get* app "/")]
+          status => 500
+          body => {:type "unknown-exception"
+                   :class "java.lang.RuntimeException"}))
+      (with-out-str
+        (get* app "/")) => "WARN Error-handler arity has been changed.\n")))
+
+(s/defn schema-error [a :- s/Int]
+  {:bar a})
+
+(fact "handling schema.core/error"
+  (let [app (api
+              {:exceptions {:handlers {:schema.core/error ex/schema-error-handler}}}
+              (GET "/:a" []
+                :path-params [a :- s/Str]
+                (ok (s/with-fn-validation (schema-error a)))))]
+    (let [[status body] (get* app "/foo")]
+      status => 400
+      body => (contains {:errors vector?}))))
+
+(fact "ring-swagger options"
+  (let [app (api
+              {:ring-swagger {:default-response-description-fn status/get-description}}
+              (swagger-routes)
+              (GET "/ping" []
+                :responses {500 nil}
+                identity))]
+    (-> app get-spec :paths vals first :get :responses :500 :description)
+    => "There was an internal server error."))
+
+(fact "path-for"
+  (fact "simple case"
+    (let [app (api
+                (GET "/api/pong" []
+                  :name :pong
+                  (ok {:pong "pong"}))
+                (GET "/api/ping" []
+                  (moved-permanently (path-for :pong))))]
+      (fact "path-for works"
+        (let [[status body] (get* app "/api/ping" {})]
+          status => 200
+          body => {:pong "pong"}))))
+
+  (fact "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}))))
+
+  (fact "https://github.com/metosin/compojure-api/issues/150"
+    (let [app (api
+                (GET "/companies/:company-id/refresh" []
+                  :path-params [company-id :- s/Int]
+                  :name :refresh-company
+                  :return String
+                  (ok (path-for :refresh-company {:company-id company-id}))))]
+      (fact "path-for resolution"
+        (let [[status body] (get* app "/companies/4/refresh")]
+          status => 200
+          body => "/companies/4/refresh"))))
+
+  (fact "multiple routes with same name fail at compile-time"
+    (let [app' `(api
+                  (GET "/api/pong" []
+                    :name :pong
+                    identity)
+                  (GET "/api/ping" []
+                    :name :pong
+                    identity))]
+      (eval app') => (throws RuntimeException))))
+
+
+(fact "swagger-spec-path"
+  (fact "defaults to /swagger.json"
+    (let [app (api (swagger-routes))]
+      (swagger/swagger-spec-path app) => "/swagger.json"))
+  (fact "follows defined path"
+    (let [app (api (swagger-routes {:spec "/api/api-docs/swagger.json"}))]
+      (swagger/swagger-spec-path app) => "/api/api-docs/swagger.json")))
+
+(defrecord NonSwaggerRecord [data])
+
+(fact "api validation"
+
+  (fact "a swagger api with valid swagger records"
+    (let [app (api
+                (swagger-routes)
+                (GET "/ping" []
+                  :return {:data s/Str}
+                  (ok {:data "ping"})))]
+
+      (fact "works"
+        (let [[status body] (get* app "/ping")]
+          status => 200
+          body => {:data "ping"}))
+
+      (fact "the api is valid"
+        (validator/validate app) => app)))
+
+  (fact "a swagger api with invalid swagger records"
+    (let [app (api
+                (swagger-routes)
+                (GET "/ping" []
+                  :return NonSwaggerRecord
+                  (ok (->NonSwaggerRecord "ping"))))]
+
+      (fact "works"
+        (let [[status body] (get* app "/ping")]
+          status => 200
+          body => {:data "ping"}))
+
+      (fact "the api is invalid"
+        (validator/validate app)
+        => (throws
+             IllegalArgumentException
+             (str
+               "don't know how to convert class compojure.api.integration_test.NonSwaggerRecord "
+               "into a Swagger Schema. Check out ring-swagger docs for details.")))))
+
+  (fact "a non-swagger api with invalid swagger records"
+    (let [app (api
+                (GET "/ping" []
+                  :return NonSwaggerRecord
+                  (ok (->NonSwaggerRecord "ping"))))]
+
+      (fact "works"
+        (let [[status body] (get* app "/ping")]
+          status => 200
+          body => {:data "ping"}))
+
+      (fact "the api is valid"
+        (validator/validate app) => app))))
+
+(fact "component integration"
+  (let [system {:magic 42}]
+    (fact "via options"
+      (let [app (api
+                  {:components system}
+                  (GET "/magic" []
+                    :components [magic]
+                    (ok {:magic magic})))]
+        (let [[status body] (get* app "/magic")]
+          status => 200
+          body => {:magic 42})))
+
+    (fact "via middleware"
+      (let [handler (api
+                      (GET "/magic" []
+                        :components [magic]
+                        (ok {:magic magic})))
+            app (mw/wrap-components handler system)]
+        (let [[status body] (get* app "/magic")]
+          status => 200
+          body => {:magic 42})))))
+
+(fact "sequential string parameters"
+  (let [app (api
+              (GET "/ints" []
+                :query-params [i :- [s/Int]]
+                (ok {:i i})))]
+    (fact "multiple values"
+      (let [[status body] (get* app "/ints?i=1&i=2&i=3")]
+        status => 200
+        body => {:i [1, 2, 3]}))
+    (fact "single value"
+      (let [[status body] (get* app "/ints?i=42")]
+        status => 200
+        body => {:i [42]}))))
+
+(fact ":swagger params just for ducumentation"
+  (fact "compile-time values"
+    (let [app (api
+                (swagger-routes)
+                (GET "/route" [q]
+                  :swagger {:x-name :boolean
+                            :operationId "echoBoolean"
+                            :description "Ehcoes a boolean"
+                            :parameters {:query {:q s/Bool}}}
+                  (ok {:q q})))]
+
+      (fact "there is no coercion"
+        (let [[status body] (get* app "/route" {:q "kikka"})]
+          status => 200
+          body => {:q "kikka"}))
+
+      (fact "swagger-docs are generated"
+        (-> app get-spec :paths vals first :get)
+        => (contains
+             {:x-name "boolean"
+              :operationId "echoBoolean"
+              :description "Ehcoes a boolean"
+              :parameters [{:description ""
+                            :in "query"
+                            :name "q"
+                            :required true
+                            :type "boolean"}]}))))
+  (fact "run-time values"
+    (let [runtime-data {:x-name :boolean
+                        :operationId "echoBoolean"
+                        :description "Ehcoes a boolean"
+                        :parameters {:query {:q s/Bool}}}
+          app (api
+                (swagger-routes)
+                (GET "/route" [q]
+                  :swagger runtime-data
+                  (ok {:q q})))]
+
+      (fact "there is no coercion"
+        (let [[status body] (get* app "/route" {:q "kikka"})]
+          status => 200
+          body => {:q "kikka"}))
+
+      (fact "swagger-docs are generated"
+        (-> app get-spec :paths vals first :get)
+        => (contains
+             {:x-name "boolean"
+              :operationId "echoBoolean"
+              :description "Ehcoes a boolean"
+              :parameters [{:description ""
+                            :in "query"
+                            :name "q"
+                            :required true
+                            :type "boolean"}]})))))
+
+(fact "swagger-docs via api options, #218"
+  (let [routes (routes
+                 (context "/api" []
+                   (GET "/ping" []
+                     :summary "ping"
+                     (ok {:message "pong"}))
+                   (POST "/pong" []
+                     :summary "pong"
+                     (ok {:message "ping"})))
+                 (ANY "*" []
+                   (ok {:message "404"})))
+        api1 (api {:swagger {:spec "/swagger.json", :ui "/"}} routes)
+        api2 (api (swagger-routes) routes)]
+
+    (fact "both generate same swagger-spec"
+      (get-spec api1) => (get-spec api2))
+
+    (fact "not-found handler works"
+      (second (get* api1 "/missed")) => {:message "404"}
+      (second (get* api2 "/missed")) => {:message "404"})))
+
+(fact "more swagger-data can be (deep-)merged in - either via swagger-docs at runtime via mws, fixes #170"
+  (let [app (api
+              (middleware [[rsm/wrap-swagger-data {:paths {"/runtime" {:get {}}}}]]
+                (swagger-routes
+                  {:data
+                   {:info {:version "2.0.0"}
+                    :paths {"/extra" {:get {}}}}})
+                (GET "/normal" [] (ok))))]
+    (get-spec app) => (contains
+                        {:paths (just
+                                  {"/normal" irrelevant
+                                   "/extra" irrelevant
+                                   "/runtime" irrelevant})})))
+
+
+(s/defschema Foo {:a [s/Keyword]})
+
+(defapi with-defapi
+  (swagger-routes)
+  (GET "/foo" []
+    :return Foo
+    (ok {:a "foo"})))
+
+(defn with-api []
+  (api
+    (swagger-routes)
+    (GET "/foo" []
+      :return Foo
+      (ok {:a "foo"}))))
+
+(fact "defapi & api define same results, #159"
+  (get-spec with-defapi) => (get-spec (with-api)))
+
+(fact "coercion api change in 1.0.0 migration test"
+
+  (fact "with defaults"
+    (let [app (api
+                (GET "/ping" []
+                  :return s/Bool
+                  (ok 1)))]
+      (let [[status] (get* app "/ping")]
+        status => 500)))
+
+  (fact "with pre 1.0.0 syntax, api can't be created (with a nice error message)"
+    (let [app' `(api
+                  {:coercion (dissoc mw/default-coercion-matchers :response)}
+                  (GET "/ping" []
+                    :return s/Bool
+                    (ok 1)))]
+      (eval app') => (throws AssertionError)))
+
+  (fact "with post 1.0.0 syntax, works ok"
+    (let [app (api
+                {:coercion (constantly (dissoc mw/default-coercion-matchers :response))}
+                (GET "/ping" []
+                  :return s/Bool
+                  (ok 1)))]
+      (let [[status body] (get* app "/ping")]
+        status => 200
+        body => 1))))
+
+(fact "handling invalid routes with api"
+  (let [invalid-routes (routes (constantly nil))]
+
+    (fact "by default, logs the exception"
+      (api invalid-routes) => truthy
+      (provided
+        (compojure.api.impl.logging/log! :warn irrelevant) => irrelevant :times 1))
+
+    (fact "ignoring invalid routes doesn't log"
+      (api {:api {:invalid-routes-fn nil}} invalid-routes) => truthy
+      (provided
+        (compojure.api.impl.logging/log! :warn irrelevant) => irrelevant :times 0))
+
+    (fact "throwing exceptions"
+      (api {:api {:invalid-routes-fn routes/fail-on-invalid-child-routes}} invalid-routes)) => throws))
+
+(defmethod compojure.api.meta/restructure-param ::deprecated-middlewares-test [_ _ acc]
+  (assoc acc :middlewares [(constantly nil)]))
+
+(defmethod compojure.api.meta/restructure-param ::deprecated-parameters-test [_ _ acc]
+  (assoc-in acc [:parameters :parameters :query] {:a String}))
+
+(defn msg-or-cause-msg [msg-re]
+  (fn [e]
+    ;; In Clojure 1.10+, macroexpansion exceptions get wrapped in another exception.
+    ;; In that case we will look at the cause.
+    (boolean (or (re-find msg-re (.getMessage e))
+                 (re-find msg-re (.getMessage (.getCause e)))))))
+
+(fact "old middlewares restructuring"
+
+  (fact ":middlewares"
+    (eval '(GET "/foo" []
+             ::deprecated-middlewares-test true
+             (ok)))
+    => (throws (msg-or-cause-msg #":middlewares is deprecated with 1.0.0, use :middleware instead.")))
+  (fact ":parameters"
+    (eval '(GET "/foo" []
+             ::deprecated-parameters-test true
+             (ok)))
+    => (throws (msg-or-cause-msg #":parameters is deprecated with 1.0.0, use :swagger instead."))))
+
+(fact "using local symbols for restructuring params"
+  (let [responses {400 {:schema {:fail s/Str}}}
+        app (api
+              {:swagger {:spec "/swagger.json"
+                         :data {:info {:version "2.0.0"}}}}
+              (GET "/a" []
+                :responses responses
+                :return {:ok s/Str}
+                (ok))
+              (GET "/b" []
+                :responses (assoc responses 500 {:schema {:m s/Str}})
+                :return {:ok s/Str}
+                (ok)))
+        paths (:paths (get-spec app))]
+
+    (get-in paths ["/a" :get :responses])
+    => (just {:400 (just {:schema anything :description ""})
+              :200 (just {:schema anything :description ""})})
+
+    (get-in paths ["/b" :get :responses])
+    => (just {:400 (just {:schema anything :description ""})
+              :200 (just {:schema anything :description ""})
+              :500 (just {:schema anything :description ""})})))
+
+(fact "when functions are returned"
+  (let [wrap-mw-params (fn [handler value]
+                         (fn [request]
+                           (handler
+                             (update request ::mw #(str % value)))))]
+    (fact "from endpoint"
+      (let [app (GET "/ping" []
+                  :middleware [[wrap-mw-params "1"]]
+                  :query-params [{a :- s/Str "a"}]
+                  (fn [req] (str (::mw req) a)))]
+
+        (app {:request-method :get, :uri "/ping", :query-params {}}) => (contains {:body "1a"})
+        (app {:request-method :get, :uri "/ping", :query-params {:a "A"}}) => (contains {:body "1A"})))
+
+    (fact "from endpoint under context"
+      (let [app (context "/api" []
+                  :middleware [[wrap-mw-params "1"]]
+                  :query-params [{a :- s/Str "a"}]
+                  (GET "/ping" []
+                    :middleware [[wrap-mw-params "2"]]
+                    :query-params [{b :- s/Str "b"}]
+                    (fn [req] (str (::mw req) a b))))]
+
+        (app {:request-method :get, :uri "/api/ping", :query-params {}}) => (contains {:body "12ab"})
+        (app {:request-method :get, :uri "/api/ping", :query-params {:a "A"}}) => (contains {:body "12Ab"})
+        (app {:request-method :get, :uri "/api/ping", :query-params {:a "A", :b "B"}}) => (contains {:body "12AB"})))))
+
+(defn check-for-response-handler
+  "This response-validation handler checks for the existence of :response in its input. If it's there, it
+  returns status 200, including the value that was origingally returned. Otherwise it returns 404."
+  [^Exception e data request]
+  (if (:response data)
+    (ok {:message "Found :response in data!" :attempted-body (get-in data [:response :body])})
+    (not-found "No :response key present in data!")))
+
+(fact "response-validation handler has access to response value that failed coercion"
+  (let [incorrect-return-value {:incorrect "response"}
+        app (api
+              {:exceptions {:handlers {::ex/response-validation check-for-response-handler}}}
+              (swagger-routes)
+              (GET "/test-response" []
+                :return {:correct s/Str}
+                ; This should fail and trigger our error handler
+                (ok incorrect-return-value)))]
+
+    (fact "return case, valid request & valid model"
+      (let [[status body] (get* app "/test-response")]
+        status => 200
+        (:attempted-body body) => incorrect-return-value))))
+
+(fact "correct swagger parameter order with small number or parameters, #224"
+  (let [app (api
+              (swagger-routes)
+              (GET "/ping" []
+                :query-params [a b c d e]
+                (ok {:a a, :b b, :c c, :d d, :e e})))]
+    (fact "api works"
+      (let [[status body] (get* app "/ping" {:a "A" :b "B" :c "C" :d "D" :e "E"})]
+        status => 200
+        body => {:a "A" :b "B" :c "C" :d "D" :e "E"}))
+    (fact "swagger parameters are in correct order"
+      (-> app get-spec :paths (get "/ping") :get :parameters (->> (map (comp keyword :name)))) => [:a :b :c :d :e])))
+
+(fact "empty top-level route, #https://github.com/metosin/ring-swagger/issues/92"
+  (let [app (api
+              {:swagger {:spec "/swagger.json"}}
+              (GET "/" [] (ok {:kikka "kukka"})))]
+    (fact "api works"
+      (let [[status body] (get* app "/")]
+        status => 200
+        body => {:kikka "kukka"}))
+    (fact "swagger docs"
+      (-> app get-spec :paths keys) => ["/"])))
+
+(fact "describe works on anonymous bodys, #168"
+  (let [app (api
+              (swagger-routes)
+              (POST "/" []
+                :body [body (describe {:kikka [{:kukka String}]} "kikkas")]
+                (ok body)))]
+    (fact "description is in place"
+      (-> app get-spec :paths (get "/") :post :parameters first)
+      => (contains {:description "kikkas"}))))
+
+(facts "swagger responses headers are mapped correctly, #232"
+  (let [app (api
+              (swagger-routes)
+              (context "/resource" []
+                (resource
+                  {:get {:responses {200 {:schema {:size s/Str}
+                                          :description "size"
+                                          :headers {"X-men" (describe s/Str "mutant")}}}}})))]
+    (-> app get-spec :paths vals first :get :responses :200 :headers)
+    => {:X-men {:description "mutant", :type "string"}}))
+
+(facts "api-middleware can be disabled"
+  (let [app (api
+              {:api {:disable-api-middleware? true}}
+              (swagger-routes)
+              (GET "/params" [x] (ok {:x x}))
+              (GET "/throw" [] (throw (RuntimeException. "kosh"))))]
+
+    (fact "json-parsing & wrap-params is off"
+      (let [[status body] (raw-get* app "/params" {:x 1})]
+        status => 200
+        body => {:x nil}))
+
+    (fact "exceptions are not caught"
+      (raw-get* app "/throw") => throws)))
+
+(facts "custom formats contribute to Swagger :consumes & :produces"
+  (let [custom-json (format-response/make-encoder json "application/vnd.vendor.v1+json")
+        app (api
+              {:swagger {:spec "/swagger.json"}
+               :format {:formats [custom-json :json]}}
+              (POST "/echo" []
+                :body [data {:kikka s/Str}]
+                (ok data)))]
+
+    (fact "it works"
+      (let [response (app {:uri "/echo"
+                           :request-method :post
+                           :body (json-stream {:kikka "kukka"})
+                           :headers {"content-type" "application/vnd.vendor.v1+json"
+                                     "accept" "application/vnd.vendor.v1+json"}})]
+
+        (-> response :body slurp) => (json {:kikka "kukka"})
+        (-> response :headers) => (contains {"Content-Type" "application/vnd.vendor.v1+json; charset=utf-8"})))
+
+    (fact "spec is correct"
+      (get-spec app) => (contains
+                          {:produces ["application/vnd.vendor.v1+json" "application/json"]
+                           :consumes ["application/vnd.vendor.v1+json" "application/json"]}))))
+
+(fact "static contexts work"
+  (let [app (context "/:a" [a]
+              (GET "/:b" [b]
+                (ok [a b])))]
+    (app {:request-method :get, :uri "/a/b"}) => (contains {:body ["a" "b"]})
+    (app {:request-method :get, :uri "/a/c"}) => (contains {:body ["a" "c"]})))
diff --git a/test-suites/compojure1/test/compojure/api/meta_test.clj b/test-suites/compojure1/test/compojure/api/meta_test.clj
new file mode 100644
index 00000000..68c1a03d
--- /dev/null
+++ b/test-suites/compojure1/test/compojure/api/meta_test.clj
@@ -0,0 +1,7 @@
+(ns compojure.api.meta-test
+  (:require [compojure.api.meta :refer :all]
+            [midje.sweet :refer :all]))
+
+(fact "src-coerce! with deprecated types"
+  (src-coerce! nil nil :query) => (throws AssertionError)
+  (src-coerce! nil nil :json) => (throws AssertionError))
diff --git a/test-suites/compojure1/test/compojure/api/middleware_test.clj b/test-suites/compojure1/test/compojure/api/middleware_test.clj
new file mode 100644
index 00000000..5e1fc64e
--- /dev/null
+++ b/test-suites/compojure1/test/compojure/api/middleware_test.clj
@@ -0,0 +1,88 @@
+(ns compojure.api.middleware-test
+  (:require [compojure.api.middleware :refer :all]
+            [compojure.api.exception :as ex]
+            [midje.sweet :refer :all]
+            [ring.util.http-response :refer [ok]]
+            [ring.util.http-status :as status]
+            ring.util.test
+            [slingshot.slingshot :refer [throw+]])
+  (:import [java.io PrintStream ByteArrayOutputStream]))
+
+(defmacro without-err
+  "Evaluates exprs in a context in which *err* is bound to a fresh
+  StringWriter.  Returns the string created by any nested printing
+  calls."
+  [& body]
+  `(let [s# (PrintStream. (ByteArrayOutputStream.))
+         err# (System/err)]
+     (System/setErr s#)
+     (try
+       ~@body
+       (finally
+         (System/setErr err#)))))
+
+(facts serializable?
+  (tabular
+    (fact
+      (serializable? nil
+                     {:body ?body
+                      :compojure.api.meta/serializable? ?serializable?}) => ?res)
+    ?body ?serializable? ?res
+    5 true true
+    5 false false
+    "foobar" true true
+    "foobar" false false
+
+    {:foobar "1"} false true
+    {:foobar "1"} true true
+    [1 2 3] false true
+    [1 2 3] true true
+
+    (ring.util.test/string-input-stream "foobar") false false))
+
+(def default-options (:exceptions api-middleware-defaults))
+
+(facts "wrap-exceptions"
+  (with-out-str
+    (without-err
+      (let [exception (RuntimeException. "kosh")
+            exception-class (.getName (.getClass exception))
+            handler (-> (fn [_] (throw exception))
+                        (wrap-exceptions default-options))]
+
+        (fact "converts exceptions into safe internal server errors"
+          (handler {}) => (contains {:status status/internal-server-error
+                                     :body (contains {:class exception-class
+                                                      :type "unknown-exception"})})))))
+
+  (with-out-str
+    (without-err
+      (fact "Slingshot exception map type can be matched"
+        (let [handler (-> (fn [_] (throw+ {:type ::test} (RuntimeException. "kosh")))
+                          (wrap-exceptions (assoc-in default-options [:handlers ::test] (fn [ex _ _] {:status 500 :body "hello"}))))]
+          (handler {}) => (contains {:status status/internal-server-error
+                                     :body "hello"})))))
+
+  (without-err
+    (fact "Default handler logs exceptions to console"
+      (let [handler (-> (fn [_] (throw (RuntimeException. "kosh")))
+                        (wrap-exceptions default-options))]
+        (with-out-str (handler {})) => "ERROR kosh\n")))
+
+  (without-err
+    (fact "Default request-parsing handler does not log messages"
+      (let [handler (-> (fn [_] (throw (ex-info "Error parsing request" {:type ::ex/request-parsing} (RuntimeException. "Kosh"))))
+                        (wrap-exceptions default-options))]
+        (with-out-str (handler {})) => "")))
+
+  (without-err
+    (fact "Logging can be added to a exception handler"
+      (let [handler (-> (fn [_] (throw (ex-info "Error parsing request" {:type ::ex/request-parsing} (RuntimeException. "Kosh"))))
+                        (wrap-exceptions (assoc-in default-options [:handlers ::ex/request-parsing] (ex/with-logging ex/request-parsing-handler :info))))]
+        (with-out-str (handler {})) => "INFO Error parsing request\n"))))
+
+(facts "compose-middeleware strips nils aways. #228"
+  (let [times2-mw (fn [handler]
+                    (fn [request]
+                      (* 2 (handler request))))]
+    (((compose-middleware [nil times2-mw nil]) (constantly 3)) anything) => 6))
diff --git a/test-suites/compojure1/test/compojure/api/perf_test.clj b/test-suites/compojure1/test/compojure/api/perf_test.clj
new file mode 100644
index 00000000..ce960b2a
--- /dev/null
+++ b/test-suites/compojure1/test/compojure/api/perf_test.clj
@@ -0,0 +1,237 @@
+(ns compojure.api.perf-test
+  (:require [compojure.api.sweet :refer :all]
+            [compojure.api.test-utils :as h]
+            [criterium.core :as cc]
+            [ring.util.http-response :refer :all]
+            [schema.core :as s]
+            [clojure.java.io :as io]
+            [cheshire.core :as json]
+            [cheshire.core :as cheshire])
+  (:import (java.io ByteArrayInputStream)))
+
+;;
+;; start repl with `lein perf repl`
+;; perf measured with the following setup:
+;;
+;; Model Name:            MacBook Pro
+;; Model Identifier:      MacBookPro11,3
+;; Processor Name:        Intel Core i7
+;; Processor Speed:       2,5 GHz
+;; Number of Processors:  1
+;; Total Number of Cores: 4
+;; L2 Cache (per Core):   256 KB
+;; L3 Cache:              6 MB
+;; Memory:                16 GB
+;;
+
+(defn title [s]
+  (println
+    (str "\n\u001B[35m"
+         (apply str (repeat (+ 6 (count s)) "#"))
+         "\n## " s " ##\n"
+         (apply str (repeat (+ 6 (count s)) "#"))
+         "\u001B[0m\n")))
+
+(defn post* [app uri json]
+  (->
+    (app {:uri uri
+          :request-method :post
+          :content-type "application/json"
+          :body (io/input-stream (.getBytes json))})
+    :body
+    slurp))
+
+(defn parse [s] (json/parse-string s true))
+
+(s/defschema Order {:id s/Str
+                    :name s/Str
+                    (s/optional-key :description) s/Str
+                    :address (s/maybe {:street s/Str
+                                       :country (s/enum "FI" "PO")})
+                    :orders [{:name #"^k"
+                              :price s/Any
+                              :shipping s/Bool}]})
+
+(defn bench []
+
+  ; 27µs
+  ; 27µs (-0%)
+  ; 25µs (1.0.0)
+  (let [app (api
+              (GET "/30" []
+                (ok {:result 30})))
+        call #(h/get* app "/30")]
+
+    (title "GET JSON")
+    (assert (= {:result 30} (second (call))))
+    (cc/bench (call)))
+
+  ;; 73µs
+  ;; 53µs (-27%)
+  ;; 50µs (1.0.0)
+  (let [app (api
+              (POST "/plus" []
+                :return {:result s/Int}
+                :body-params [x :- s/Int, y :- s/Int]
+                (ok {:result (+ x y)})))
+        data (h/json {:x 10, :y 20})
+        call #(post* app "/plus" data)]
+
+    (title "JSON POST with 2-way coercion")
+    (assert (= {:result 30} (parse (call))))
+    (cc/bench (call)))
+
+  ;; 85µs
+  ;; 67µs (-21%)
+  ;; 66µs (1.0.0)
+  (let [app (api
+              (context "/a" []
+                (context "/b" []
+                  (context "/c" []
+                    (POST "/plus" []
+                      :return {:result s/Int}
+                      :body-params [x :- s/Int, y :- s/Int]
+                      (ok {:result (+ x y)}))))))
+        data (h/json {:x 10, :y 20})
+        call #(post* app "/a/b/c/plus" data)]
+
+    (title "JSON POST with 2-way coercion + contexts")
+    (assert (= {:result 30} (parse (call))))
+    (cc/bench (call)))
+
+  ;; 266µs
+  ;; 156µs (-41%)
+  ;; 146µs (1.0.0)
+  (let [app (api
+              (POST "/echo" []
+                :return Order
+                :body [order Order]
+                (ok order)))
+        data (h/json {:id "123"
+                      :name "Tommi's order"
+                      :description "Totally great order"
+                      :address {:street "Randomstreet 123"
+                                :country "FI"}
+                      :orders [{:name "k1"
+                                :price 123.0
+                                :shipping true}
+                               {:name "k2"
+                                :price 42.0
+                                :shipping false}]})
+        call #(post* app "/echo" data)]
+
+    (title "JSON POST with nested data")
+    (s/validate Order (parse (call)))
+    (cc/bench (call))))
+
+(defn resource-bench []
+
+  (let [resource-map {:post {:responses {200 {:schema {:result s/Int}}}
+                             :parameters {:body-params {:x s/Int, :y s/Int}}
+                             :handler (fn [{{:keys [x y]} :body-params}]
+                                        (ok {:result (+ x y)}))}}]
+
+    ;; 62µs
+    (let [my-resource (resource resource-map)
+          app (api
+                (context "/plus" []
+                  my-resource))
+          data (h/json {:x 10, :y 20})
+          call #(post* app "/plus" data)]
+
+      (title "JSON POST to pre-defined resource with 2-way coercion")
+      (assert (= {:result 30} (parse (call))))
+      (cc/bench (call)))
+
+    ;; 68µs
+    (let [app (api
+                (context "/plus" []
+                  (resource resource-map)))
+          data (h/json {:x 10, :y 20})
+          call #(post* app "/plus" data)]
+
+      (title "JSON POST to inlined resource with 2-way coercion")
+      (assert (= {:result 30} (parse (call))))
+      (cc/bench (call)))
+
+    ;; 26µs
+    (let [my-resource (resource resource-map)
+          app my-resource
+          data {:x 10, :y 20}
+          call #(app {:request-method :post :uri "/irrelevant" :body-params data})]
+
+      (title "direct POST to pre-defined resource with 2-way coercion")
+      (assert (= {:result 30} (:body (call))))
+      (cc/bench (call)))
+
+    ;; 30µs
+    (let [my-resource (resource resource-map)
+          app (context "/plus" []
+                my-resource)
+          data {:x 10, :y 20}
+          call #(app {:request-method :post :uri "/plus" :body-params data})]
+
+      (title "POST to pre-defined resource with 2-way coercion")
+      (assert (= {:result 30} (:body (call))))
+      (cc/bench (call)))
+
+    ;; 40µs
+    (let [app (context "/plus" []
+                (resource resource-map))
+          data {:x 10, :y 20}
+          call #(app {:request-method :post :uri "/plus" :body-params data})]
+
+      (title "POST to inlined resource with 2-way coercion")
+      (assert (= {:result 30} (:body (call))))
+      (cc/bench (call)))))
+
+(defn e2e-json-comparison-different-payloads []
+  (let [json-request (fn [data]
+                       {:uri "/echo"
+                        :request-method :post
+                        :headers {"content-type" "application/json"
+                                  "accept" "application/json"}
+                        :body (cheshire/generate-string data)})
+        request-stream (fn [request]
+                         (let [b (.getBytes ^String (:body request))]
+                           (fn []
+                             (assoc request :body (ByteArrayInputStream. b)))))
+        app (api
+              (POST "/echo" []
+                :body [body s/Any]
+                (ok body)))]
+    (doseq [file ["dev-resources/json/json10b.json"
+                  "dev-resources/json/json100b.json"
+                  "dev-resources/json/json1k.json"
+                  "dev-resources/json/json10k.json"
+                  "dev-resources/json/json100k.json"]
+            :let [data (cheshire/parse-string (slurp file))
+                  request (json-request data)
+                  request! (request-stream request)]]
+
+      "10b"
+      ;; 42µs
+
+      "100b"
+      ;; 79µs
+
+      "1k"
+      ;; 367µs
+
+      "10k"
+      ;; 2870µs
+
+      "100k"
+      ;; 10800µs
+
+      (title file)
+      (cc/bench (-> (request!) app :body slurp)))))
+
+(comment
+  (bench)
+  (resource-bench)
+  (e2e-json-comparison-different-payloads))
+
+(comment
+  (bench)
+  (resource-bench))
diff --git a/test-suites/compojure1/test/compojure/api/resource_test.clj b/test-suites/compojure1/test/compojure/api/resource_test.clj
new file mode 100644
index 00000000..fbe6ccb5
--- /dev/null
+++ b/test-suites/compojure1/test/compojure/api/resource_test.clj
@@ -0,0 +1,206 @@
+(ns compojure.api.resource-test
+  (:require [compojure.api.sweet :refer :all]
+            [compojure.api.test-utils :refer :all]
+            [plumbing.core :refer [fnk]]
+            [midje.sweet :refer :all]
+            [ring.util.http-response :refer :all]
+            [schema.core :as s])
+  (:import [clojure.lang ExceptionInfo]))
+
+(defn has-body [expected]
+  (fn [{:keys [body]}]
+    (= body expected)))
+
+(def request-validation-failed?
+  (throws ExceptionInfo #"Request validation failed"))
+
+(def response-validation-failed?
+  (throws ExceptionInfo #"Response validation failed"))
+
+(facts "resource definitions"
+
+  (fact "only top-level handler"
+    (let [handler (resource
+                    {:handler (constantly (ok {:total 10}))})]
+
+      (fact "paths and methods don't matter"
+        (handler {:request-method :get, :uri "/"}) => (has-body {:total 10})
+        (handler {:request-method :head, :uri "/kikka"}) => (has-body {:total 10}))))
+
+  (fact "top-level parameter coercions"
+    (let [handler (resource
+                    {:parameters {:query-params {:x Long}}
+                     :handler (fnk [[:query-params x]]
+                                (ok {:total x}))})]
+
+      (handler {:request-method :get}) => request-validation-failed?
+      (handler {:request-method :get, :query-params {:x "1"}}) => (has-body {:total 1})
+      (handler {:request-method :get, :query-params {:x "1", :y "2"}}) => (has-body {:total 1})))
+
+  (fact "top-level and operation-level parameter coercions"
+    (let [handler (resource
+                    {:parameters {:query-params {:x Long}}
+                     :get {:parameters {:query-params {(s/optional-key :y) Long}}}
+                     :handler (fnk [[:query-params x {y 0}]]
+                                (ok {:total (+ x y)}))})]
+
+      (handler {:request-method :get}) => request-validation-failed?
+      (handler {:request-method :get, :query-params {:x "1"}}) => (has-body {:total 1})
+      (handler {:request-method :get, :query-params {:x "1", :y "a"}}) => request-validation-failed?
+      (handler {:request-method :get, :query-params {:x "1", :y "2"}}) => (has-body {:total 3})
+
+      (fact "non-matching operation level parameters are not used"
+        (handler {:request-method :post, :query-params {:x "1"}}) => (has-body {:total 1})
+        (handler {:request-method :post, :query-params {:x "1", :y "2"}}) => (throws ClassCastException))))
+
+  (fact "operation-level handlers"
+    (let [handler (resource
+                    {:parameters {:query-params {:x Long}}
+                     :get {:parameters {:query-params {(s/optional-key :y) Long}}
+                           :handler (fnk [[:query-params x {y 0}]]
+                                      (ok {:total (+ x y)}))}
+                     :post {:parameters {:query-params {:z Long}}}})]
+
+      (handler {:request-method :get}) => request-validation-failed?
+      (handler {:request-method :get, :query-params {:x "1"}}) => (has-body {:total 1})
+      (handler {:request-method :get, :query-params {:x "1", :y "a"}}) => request-validation-failed?
+      (handler {:request-method :get, :query-params {:x "1", :y "2"}}) => (has-body {:total 3})
+
+      (fact "if no handler is found, nil is returned"
+        (handler {:request-method :post, :query-params {:x "1"}}) => nil)))
+
+  (fact "handler preference"
+    (let [handler (resource
+                    {:get {:handler (constantly (ok {:from "get"}))}
+                     :handler (constantly (ok {:from "top"}))})]
+
+      (handler {:request-method :get}) => (has-body {:from "get"})
+      (handler {:request-method :post}) => (has-body {:from "top"})))
+
+  (fact "resource without coercion"
+    (let [handler (resource
+                    {:get {:parameters {:query-params {(s/optional-key :y) Long
+                                                       (s/optional-key :x) Long}}
+                           :handler (fn [{{:keys [x y]} :query-params}]
+                                      (ok {:x x
+                                           :y y}))}}
+                    {:coercion (constantly nil)})]
+
+      (handler {:request-method :get}) => (has-body {:x nil, :y nil})
+      (handler {:request-method :get, :query-params {:x "1"}}) => (has-body {:x "1", :y nil})
+      (handler {:request-method :get, :query-params {:x "1", :y "a"}}) => (has-body {:x "1", :y "a"})
+      (handler {:request-method :get, :query-params {:x 1, :y 2}}) => (has-body {:x 1, :y 2})))
+
+  (fact "parameter mappings"
+    (let [handler (resource
+                    {:get {:parameters {:query-params {:q s/Str}
+                                        :body-params {:b s/Str}
+                                        :form-params {:f s/Str}
+                                        :header-params {:h s/Str}
+                                        :path-params {:p s/Str}}
+                           :handler (fn [request]
+                                      (ok (select-keys request [:query-params
+                                                                :body-params
+                                                                :form-params
+                                                                :header-params
+                                                                :path-params])))}})]
+
+      (handler {:request-method :get
+                :query-params {:q "q"}
+                :body-params {:b "b"}
+                :form-params {:f "f"}
+                ;; the ring headers
+                :headers {"h" "h"}
+                ;; compojure routing
+                :route-params {:p "p"}}) => (has-body {:query-params {:q "q"}
+                                                       :body-params {:b "b"}
+                                                       :form-params {:f "f"}
+                                                       :header-params {:h "h"}
+                                                       :path-params {:p "p"}})))
+
+  (fact "response coercion"
+    (let [handler (resource
+                    {:responses {200 {:schema {:total (s/constrained Long pos? 'pos)}}}
+                     :parameters {:query-params {:x Long}}
+                     :get {:responses {200 {:schema {:total (s/constrained Long #(>= % 10) 'gte10)}}}
+                           :handler (fnk [[:query-params x]]
+                                      (ok {:total x}))}
+                     :handler (fnk [[:query-params x]]
+                                (ok {:total x}))})]
+
+      (handler {:request-method :get}) => request-validation-failed?
+      (handler {:request-method :get, :query-params {:x "-1"}}) => response-validation-failed?
+      (handler {:request-method :get, :query-params {:x "1"}}) => response-validation-failed?
+      (handler {:request-method :get, :query-params {:x "10"}}) => (has-body {:total 10})
+      (handler {:request-method :post, :query-params {:x "1"}}) => (has-body {:total 1}))))
+
+(fact "compojure-api routing integration"
+  (let [handler (context "/rest" []
+
+                  (GET "/no" request
+                    (ok (select-keys request [:uri :path-info])))
+
+                  (context "/context" []
+                    (resource
+                      {:handler (constantly (ok "CONTEXT"))}))
+
+                  ;; does not work
+                  (ANY "/any" []
+                    (resource
+                      {:handler (constantly (ok "ANY"))}))
+
+                  (context "/path/:id" []
+                    (resource
+                      {:parameters {:path-params {:id s/Int}}
+                       :handler (fn [request]
+                                  (ok (select-keys request [:path-params :route-params])))}))
+
+                  (resource
+                    {:get {:handler (fn [request]
+                                      (ok (select-keys request [:uri :path-info])))}}))]
+
+    (fact "normal endpoint works"
+      (handler {:request-method :get, :uri "/rest/no"}) => (has-body {:uri "/rest/no", :path-info "/no"}))
+
+    (fact "wrapped in ANY fails at runtime"
+      (handler {:request-method :get, :uri "/rest/any"}) => throws)
+
+    (fact "wrapped in context works"
+      (handler {:request-method :get, :uri "/rest/context"}) => (has-body "CONTEXT"))
+
+    (fact "path-parameters work: route-params are left untoucehed, path-params are coerced"
+      (handler {:request-method :get, :uri "/rest/path/12"}) => (has-body {:path-params {:id 12}
+                                                                           :route-params {:id "12"}}))
+
+    (fact "top-level GET works"
+      (handler {:request-method :get, :uri "/rest/in-peaces"}) => (has-body {:uri "/rest/in-peaces"
+                                                                             :path-info "/in-peaces"}))
+
+    (fact "top-level POST misses"
+      (handler {:request-method :post, :uri "/rest/in-peaces"}) => nil)))
+
+(fact "swagger-integration"
+  (let [app (api
+              (swagger-routes)
+              (context "/rest" []
+                (resource
+                  {:parameters {:query-params {:x Long}}
+                   :responses {400 {:schema (s/schema-with-name {:code s/Str} "Error")}}
+                   :get {:parameters {:query-params {:y Long}}
+                         :responses {200 {:schema (s/schema-with-name {:total Long} "Total")}}}
+                   :post {}
+                   :handler (constantly (ok {:total 1}))})))
+        spec (get-spec app)]
+
+    spec => (contains
+              {:definitions (just
+                              {:Error irrelevant
+                               :Total irrelevant})
+               :paths (just
+                        {"/rest" (just
+                                   {:get (just
+                                           {:parameters (two-of irrelevant)
+                                            :responses (just {:200 irrelevant, :400 irrelevant})})
+                                    :post (just
+                                            {:parameters (one-of irrelevant)
+                                             :responses (just {:400 irrelevant})})})})})))
diff --git a/test-suites/compojure1/test/compojure/api/routes_test.clj b/test-suites/compojure1/test/compojure/api/routes_test.clj
new file mode 100644
index 00000000..b44e100b
--- /dev/null
+++ b/test-suites/compojure1/test/compojure/api/routes_test.clj
@@ -0,0 +1,152 @@
+(ns compojure.api.routes-test
+  (:require [midje.sweet :refer :all]
+            [compojure.api.sweet :refer :all]
+            [compojure.api.routes :as routes]
+            [ring.util.http-response :refer :all]
+            [ring.util.http-predicates :refer :all]
+            [compojure.api.test-utils :refer :all]
+            [schema.core :as s])
+  (:import [java.security SecureRandom]
+           [org.joda.time LocalDate]
+           [com.fasterxml.jackson.core JsonGenerationException]))
+
+(facts "path-string"
+
+  (fact "missing path parameter"
+    (#'routes/path-string "/api/:kikka" {})
+    => (throws IllegalArgumentException))
+
+  (fact "missing serialization"
+    (#'routes/path-string "/api/:kikka" {:kikka (SecureRandom.)})
+    => (throws JsonGenerationException))
+
+  (fact "happy path"
+    (#'routes/path-string "/a/:b/:c/d/:e/f" {:b (LocalDate/parse "2015-05-22")
+                                             :c 12345
+                                             :e :kikka})
+    => "/a/2015-05-22/12345/d/kikka/f"))
+
+(fact "string-path-parameters"
+  (#'routes/string-path-parameters "/:foo.json") => {:foo String})
+
+(facts "nested routes"
+  (let [mw (fn [handler] (fn [request] (handler request)))
+        more-routes (fn [version]
+                      (routes
+                        (GET "/more" []
+                          (ok {:message version}))))
+        routes (context "/api/:version" []
+                 :path-params [version :- String]
+                 (GET "/ping" []
+                   (ok {:message (str "pong - " version)}))
+                 (POST "/ping" []
+                   (ok {:message (str "pong - " version)}))
+                 (middleware [mw]
+                   (GET "/hello" []
+                     :return {:message String}
+                     :summary "cool ping"
+                     :query-params [name :- String]
+                     (ok {:message (str "Hello, " name)}))
+                   (more-routes version)))
+        app (api
+              (swagger-routes)
+              routes)]
+
+    (fact "all routes can be invoked"
+      (let [[status body] (get* app "/api/v1/hello" {:name "Tommi"})]
+        status = 200
+        body => {:message "Hello, Tommi"})
+
+      (let [[status body] (get* app "/api/v1/ping")]
+        status = 200
+        body => {:message "pong - v1"})
+
+      (let [[status body] (get* app "/api/v2/ping")]
+        status = 200
+        body => {:message "pong - v2"})
+
+      (let [[status body] (get* app "/api/v3/more")]
+        status => 200
+        body => {:message "v3"}))
+
+    (fact "routes can be extracted at runtime"
+      (routes/get-routes app)
+      => [["/swagger.json" :get {:x-no-doc true, :x-name :compojure.api.swagger/swagger}]
+          ["/api/:version/ping" :get {:parameters {:path {:version String, s/Keyword s/Any}}}]
+          ["/api/:version/ping" :post {:parameters {:path {:version String, s/Keyword s/Any}}}]
+          ["/api/:version/hello" :get {:parameters {:query {:name String, s/Keyword s/Any}
+                                                    :path {:version String, s/Keyword s/Any}}
+                                       :responses {200 {:description "", :schema {:message String}}}
+                                       :summary "cool ping"}]
+          ["/api/:version/more" :get {:parameters {:path {:version String, s/Keyword s/Any}}}]])
+
+    (fact "swagger-docs can be generated"
+      (-> app get-spec :paths keys)
+      => ["/api/{version}/ping"
+          "/api/{version}/hello"
+          "/api/{version}/more"])))
+
+(def more-routes
+  (routes
+    (GET "/more" []
+      (ok {:gary "moore"}))))
+
+(facts "following var-routes, #219"
+  (let [routes (context "/api" [] #'more-routes)]
+    (routes/get-routes routes) => [["/api/more" :get {}]]))
+
+;; TODO: should this do something different?
+(facts "dynamic routes"
+  (let [more-routes (fn [version]
+                      (GET (str "/" version) []
+                        (ok {:message version})))
+        routes (context "/api/:version" []
+                 :path-params [version :- String]
+                 (more-routes version))
+        app (api
+              (swagger-routes)
+              routes)]
+
+    (fact "all routes can be invoked"
+      (let [[status body] (get* app "/api/v3/v3")]
+        status => 200
+        body => {:message "v3"})
+
+      (let [[status body] (get* app "/api/v6/v6")]
+        status => 200
+        body => {:message "v6"}))
+
+    (fact "routes can be extracted at runtime"
+      (routes/get-routes app)
+      => [["/swagger.json" :get {:x-no-doc true, :x-name :compojure.api.swagger/swagger}]
+          ["/api/:version/[]" :get {:parameters {:path {:version String, s/Keyword s/Any}}}]])
+
+    (fact "swagger-docs can be generated"
+      (-> app get-spec :paths keys)
+      => ["/api/{version}/[]"])))
+
+(fact "route merging"
+  (routes/get-routes (routes (routes))) => []
+  (routes/get-routes (routes (swagger-routes {:spec nil}))) => []
+  (routes/get-routes (routes (routes (GET "/ping" [] "pong")))) => [["/ping" :get {}]])
+
+(fact "invalid route options"
+  (let [r (routes (constantly nil))]
+
+    (fact "ignore 'em all"
+      (routes/get-routes r) => []
+      (routes/get-routes r nil) => []
+      (routes/get-routes r {:invalid-routes-fn nil}) => [])
+
+    (fact "log warnings"
+      (routes/get-routes r {:invalid-routes-fn routes/log-invalid-child-routes}) => []
+      (provided
+        (compojure.api.impl.logging/log! :warn irrelevant) => irrelevant :times 1))
+
+    (fact "throw exception"
+      (routes/get-routes r {:invalid-routes-fn routes/fail-on-invalid-child-routes})) => throws))
+
+(fact "context routes with compojure destructuring"
+  (let [app (context "/api" req
+              (GET "/ping" [] (ok (:magic req))))]
+    (app {:request-method :get :uri "/api/ping" :magic {:just "works"}}) => (contains {:body {:just "works"}})))
diff --git a/test-suites/compojure1/test/compojure/api/swagger_ordering_test.clj b/test-suites/compojure1/test/compojure/api/swagger_ordering_test.clj
new file mode 100644
index 00000000..55112ecf
--- /dev/null
+++ b/test-suites/compojure1/test/compojure/api/swagger_ordering_test.clj
@@ -0,0 +1,36 @@
+(ns compojure.api.swagger-ordering-test
+  (:require [midje.sweet :refer :all]
+            [compojure.api.sweet :refer :all]
+            [compojure.api.test-utils :refer :all]))
+
+(def more-routes
+  (routes
+    (GET "/6" [] identity)
+    (GET "/7" [] identity)
+    (GET "/8" [] identity)))
+
+(facts "with 10+ routes"
+  (let [app (api
+              (context "/a" []
+                (GET "/1" [] identity)
+                (GET "/2" [] identity)
+                (GET "/3" [] identity)
+                (context "/b" []
+                  (GET "/4" [] identity)
+                  (GET "/5" [] identity))
+                (context "/c" []
+                  more-routes
+                  (GET "/9" [] identity)
+                  (GET "/10" [] identity))))]
+
+    (fact "swagger-api order is maintained"
+      (keys (extract-paths app)) => ["/a/1"
+                                     "/a/2"
+                                     "/a/3"
+                                     "/a/b/4"
+                                     "/a/b/5"
+                                     "/a/c/6"
+                                     "/a/c/7"
+                                     "/a/c/8"
+                                     "/a/c/9"
+                                     "/a/c/10"])))
diff --git a/test-suites/compojure1/test/compojure/api/swagger_test.clj b/test-suites/compojure1/test/compojure/api/swagger_test.clj
new file mode 100644
index 00000000..e47cfd63
--- /dev/null
+++ b/test-suites/compojure1/test/compojure/api/swagger_test.clj
@@ -0,0 +1,187 @@
+(ns compojure.api.swagger-test
+  (:require [schema.core :as s]
+            [compojure.api.sweet :refer :all]
+            compojure.core
+            [compojure.api.test-utils :refer :all]
+            [midje.sweet :refer :all]))
+
+(defmacro optional-routes [p & body] (when p `(routes ~@body)))
+(defmacro GET+ [p & body] `(GET ~(str "/xxx" p) ~@body))
+
+(fact "extracting compojure paths"
+
+  (fact "all compojure.api.core macros are interpreted"
+    (let [app (context "/a" []
+                (routes
+                  (context "/b" []
+                    (let-routes []
+                      (GET "/c" [] identity)
+                      (POST "/d" [] identity)
+                      (PUT "/e" [] identity)
+                      (DELETE "/f" [] identity)
+                      (OPTIONS "/g" [] identity)
+                      (PATCH "/h" [] identity)))
+                  (context "/:i/:j" []
+                    (GET "/k/:l/m/:n" [] identity))))]
+
+      (extract-paths app)
+      => {"/a/b/c" {:get {}}
+          "/a/b/d" {:post {}}
+          "/a/b/e" {:put {}}
+          "/a/b/f" {:delete {}}
+          "/a/b/g" {:options {}}
+          "/a/b/h" {:patch {}}
+          "/a/:i/:j/k/:l/m/:n" {:get {:parameters {:path {:i String
+                                                          :j String
+                                                          :l String
+                                                          :n String}}}}}))
+
+  (fact "runtime code in route is NOT ignored"
+    (extract-paths
+      (context "/api" []
+        (if false
+          (GET "/true" [] identity)
+          (PUT "/false" [] identity)))) => {"/api/false" {:put {}}})
+
+  (fact "route-macros are expanded"
+    (extract-paths
+      (context "/api" []
+        (optional-routes true (GET "/true" [] identity))
+        (optional-routes false (PUT "/false" [] identity)))) => {"/api/true" {:get {}}})
+
+  (fact "endpoint-macros are expanded"
+    (extract-paths
+      (context "/api" []
+        (GET+ "/true" [] identity))) => {"/api/xxx/true" {:get {}}})
+
+  (fact "Vanilla Compojure defroutes are NOT followed"
+    (compojure.core/defroutes even-more-routes (GET "/even" [] identity))
+    (compojure.core/defroutes more-routes (context "/more" [] even-more-routes))
+    (extract-paths
+      (context "/api" []
+        (GET "/true" [] identity)
+        more-routes)) => {"/api/true" {:get {}}})
+
+  (fact "Compojure Api defroutes and def routes are followed"
+    (def even-more-routes (GET "/even" [] identity))
+    (defroutes more-routes (context "/more" [] even-more-routes))
+    (extract-paths
+      (context "/api" []
+        (GET "/true" [] identity)
+        more-routes)) => {"/api/true" {:get {}}
+                          "/api/more/even" {:get {}}})
+
+  (fact "Parameter regular expressions are discarded"
+    (extract-paths
+      (context "/api" []
+        (GET ["/:param" :param #"[a-z]+"] [] identity)))
+
+    => {"/api/:param" {:get {:parameters {:path {:param String}}}}}))
+
+(fact "context meta-data"
+  (extract-paths
+    (context "/api/:id" []
+      :summary "top-summary"
+      :path-params [id :- String]
+      :tags [:kiss]
+      (GET "/kikka" []
+        identity)
+      (context "/ipa" []
+        :summary "mid-summary"
+        :tags [:wasp]
+        (GET "/kukka/:kukka" []
+          :summary "bottom-summary"
+          :path-params [kukka :- String]
+          :tags [:venom])
+        (GET "/kakka" []
+          identity))))
+
+  => {"/api/:id/kikka" {:get {:summary "top-summary"
+                              :tags #{:kiss}
+                              :parameters {:path {:id String}}}}
+      "/api/:id/ipa/kukka/:kukka" {:get {:summary "bottom-summary"
+                                         :tags #{:venom}
+                                         :parameters {:path {:id String
+                                                             :kukka String}}}}
+      "/api/:id/ipa/kakka" {:get {:summary "mid-summary"
+                                  :tags #{:wasp}
+                                  :parameters {:path {:id String}}}}})
+
+(facts "duplicate context merge"
+  (let [app (routes
+              (context "/api" []
+                :tags [:kiss]
+                (GET "/kakka" []
+                  identity))
+              (context "/api" []
+                :tags [:kiss]
+                (GET "/kukka" []
+                  identity)))]
+    (extract-paths app)
+    => {"/api/kukka" {:get {:tags #{:kiss}}}
+        "/api/kakka" {:get {:tags #{:kiss}}}}))
+
+(def r1
+  (GET "/:id" []
+    :path-params [id :- s/Str]
+    identity))
+(def r2
+  (GET "/kukka/:id" []
+    :path-params [id :- Long]
+    identity))
+
+(facts "defined routes path-params"
+  (extract-paths (routes r1 r2))
+  => {"/:id" {:get {:parameters {:path {:id String}}}}
+      "/kukka/:id" {:get {:parameters {:path {:id Long}}}}})
+
+(fact "context meta-data"
+  (extract-paths
+    (context "/api/:id" []
+      :summary "top-summary"
+      :path-params [id :- String]
+      :tags [:kiss]
+      (GET "/kikka" []
+        identity)
+      (context "/ipa" []
+        :summary "mid-summary"
+        :tags [:wasp]
+        (GET "/kukka/:kukka" []
+          :summary "bottom-summary"
+          :path-params [kukka :- String]
+          :tags [:venom])
+        (GET "/kakka" []
+          identity))))
+
+  => {"/api/:id/kikka" {:get {:summary "top-summary"
+                              :tags #{:kiss}
+                              :parameters {:path {:id String}}}}
+      "/api/:id/ipa/kukka/:kukka" {:get {:summary "bottom-summary"
+                                         :tags #{:venom}
+                                         :parameters {:path {:id String
+                                                             :kukka String}}}}
+      "/api/:id/ipa/kakka" {:get {:summary "mid-summary"
+                                  :tags #{:wasp}
+                                  :parameters {:path {:id String}}}}})
+
+(fact "path params followed by an extension"
+  (extract-paths
+    (GET "/:foo.json" []
+      :path-params [foo :- String]
+      identity))
+  => {"/:foo.json" {:get {:parameters {:path {:foo String}}}}})
+
+(facts
+  (tabular
+    (fact "swagger-routes basePath can be changed"
+      (let [app (api (swagger-routes ?given-options))]
+        (->
+          (get* app "/swagger.json")
+          (nth 1)
+          :basePath)
+        => ?expected-base-path
+        (nth (raw-get* app "/conf.js") 1) => (str "window.API_CONF = {\"url\":\"" ?expected-swagger-docs-path "\"};")))
+    ?given-options ?expected-swagger-docs-path ?expected-base-path
+    {} "/swagger.json" "/"
+    {:data {:basePath "/app"}} "/app/swagger.json" "/app"
+    {:data {:basePath "/app"} :options {:ui {:swagger-docs "/imaginary.json"}}} "/imaginary.json" "/app"))
diff --git a/test-suites/compojure1/test/compojure/api/sweet_test.clj b/test-suites/compojure1/test/compojure/api/sweet_test.clj
new file mode 100644
index 00000000..c9605bc3
--- /dev/null
+++ b/test-suites/compojure1/test/compojure/api/sweet_test.clj
@@ -0,0 +1,209 @@
+(ns compojure.api.sweet-test
+  (:require [compojure.api.sweet :refer :all]
+            [compojure.api.test-utils :refer :all]
+            [midje.sweet :refer :all]
+            [ring.mock.request :refer :all]
+            [schema.core :as s]
+            [ring.swagger.validator :as v]))
+
+(s/defschema Band {:id s/Int
+                   :name s/Str
+                   (s/optional-key :description) (s/maybe s/Str)
+                   :toppings [(s/enum :cheese :olives :ham :pepperoni :habanero)]})
+
+(s/defschema NewBand (dissoc Band :id))
+
+(def ping-route
+  (GET "/ping" [] identity))
+
+(def app
+  (api
+    {:swagger {:spec "/swagger.json"
+               :data {: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"}}}}}
+    ping-route
+    (context "/api" []
+      ping-route
+      (GET "/bands" []
+        :name :bands
+        :return [Band]
+        :summary "Gets all Bands"
+        :description "bands bands bands"
+        :operationId "getBands"
+        identity)
+      (GET "/bands/:id" [id]
+        :return Band
+        :summary "Gets a Band"
+        :operationId "getBand"
+        identity)
+      (POST "/bands" []
+        :return Band
+        :body [band [NewBand]]
+        :summary "Adds a Band"
+        :operationId "addBand"
+        identity)
+      (GET "/query" []
+        :query-params [qp :- Boolean]
+        identity)
+      (GET "/header" []
+        :header-params [hp :- Boolean]
+        identity)
+      (POST "/form" []
+        :form-params [fp :- Boolean]
+        identity)
+      (GET "/primitive" []
+        :return String
+        identity)
+      (GET "/primitiveArray" []
+        :return [String]
+        identity))))
+
+(facts "api documentation"
+  (fact "details are generated"
+
+    (extract-paths app)
+
+    => {"/swagger.json" {:get {:x-name :compojure.api.swagger/swagger,
+                               :x-no-doc true}}
+        "/ping" {:get {}}
+        "/api/ping" {:get {}}
+        "/api/bands" {:get {:x-name :bands
+                            :operationId "getBands"
+                            :description "bands bands bands"
+                            :responses {200 {:schema [Band]
+                                             :description ""}}
+                            :summary "Gets all Bands"}
+                      :post {:operationId "addBand"
+                             :parameters {:body [NewBand]}
+                             :responses {200 {:schema Band
+                                              :description ""}}
+                             :summary "Adds a Band"}}
+        "/api/bands/:id" {:get {:operationId "getBand"
+                                :responses {200 {:schema Band
+                                                 :description ""}}
+                                :summary "Gets a Band"
+                                :parameters {:path {:id String}}}}
+        "/api/query" {:get {:parameters {:query {:qp Boolean
+                                                 s/Keyword s/Any}}}}
+        "/api/header" {:get {:parameters {:header {:hp Boolean
+                                                   s/Keyword s/Any}}}}
+        "/api/form" {:post {:parameters {:formData {:fp Boolean}}
+                            :consumes ["application/x-www-form-urlencoded"]}}
+        "/api/primitive" {:get {:responses {200 {:schema String
+                                                 :description ""}}}}
+        "/api/primitiveArray" {:get {:responses {200 {:schema [String]
+                                                      :description ""}}}}})
+
+  (fact "api-listing works"
+    (let [spec (get-spec app)]
+
+      spec => {:swagger "2.0"
+               :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"}}
+               :basePath "/"
+               :consumes ["application/json"
+                          "application/x-yaml"
+                          "application/edn"
+                          "application/transit+json"
+                          "application/transit+msgpack"],
+               :produces ["application/json"
+                          "application/x-yaml"
+                          "application/edn"
+                          "application/transit+json"
+                          "application/transit+msgpack"]
+               :paths {"/api/bands" {:get {:x-name "bands"
+                                           :operationId "getBands"
+                                           :description "bands bands bands"
+                                           :responses {:200 {:description ""
+                                                             :schema {:items {:$ref "#/definitions/Band"}
+                                                                      :type "array"}}}
+                                           :summary "Gets all Bands"}
+                                     :post {:operationId "addBand"
+                                            :parameters [{:description ""
+                                                          :in "body"
+                                                          :name "NewBand"
+                                                          :required true
+                                                          :schema {:items {:$ref "#/definitions/NewBand"}
+                                                                   :type "array"}}]
+                                            :responses {:200 {:description ""
+                                                              :schema {:$ref "#/definitions/Band"}}}
+                                            :summary "Adds a Band"}}
+                       "/api/bands/{id}" {:get {:operationId "getBand"
+                                                :parameters [{:description ""
+                                                              :in "path"
+                                                              :name "id"
+                                                              :required true
+                                                              :type "string"}]
+                                                :responses {:200 {:description ""
+                                                                  :schema {:$ref "#/definitions/Band"}}}
+                                                :summary "Gets a Band"}}
+                       "/api/query" {:get {:parameters [{:in "query"
+                                                         :name "qp"
+                                                         :description ""
+                                                         :required true
+                                                         :type "boolean"}]
+                                           :responses {:default {:description ""}}}}
+                       "/api/header" {:get {:parameters [{:in "header"
+                                                          :name "hp"
+                                                          :description ""
+                                                          :required true
+                                                          :type "boolean"}]
+                                            :responses {:default {:description ""}}}}
+                       "/api/form" {:post {:parameters [{:in "formData"
+                                                         :name "fp"
+                                                         :description ""
+                                                         :required true
+                                                         :type "boolean"}]
+                                           :responses {:default {:description ""}}
+                                           :consumes ["application/x-www-form-urlencoded"]}}
+                       "/api/ping" {:get {:responses {:default {:description ""}}}}
+                       "/api/primitive" {:get {:responses {:200 {:description ""
+                                                                 :schema {:type "string"}}}}}
+                       "/api/primitiveArray" {:get {:responses {:200 {:description ""
+                                                                      :schema {:items {:type "string"}
+                                                                               :type "array"}}}}}
+                       "/ping" {:get {:responses {:default {:description ""}}}}}
+               :definitions {:Band {:type "object"
+                                    :properties {:description {:type "string"
+                                                               :x-nullable true}
+                                                 :id {:format "int64", :type "integer"}
+                                                 :name {:type "string"}
+                                                 :toppings {:items {:enum ["olives"
+                                                                           "pepperoni"
+                                                                           "ham"
+                                                                           "cheese"
+                                                                           "habanero"]
+                                                                    :type "string"}
+                                                            :type "array"}}
+                                    :required ["id" "name" "toppings"]
+                                    :additionalProperties false}
+                             :NewBand {:type "object"
+                                       :properties {:description {:type "string"
+                                                                  :x-nullable true}
+                                                    :name {:type "string"}
+                                                    :toppings {:items {:enum ["olives"
+                                                                              "pepperoni"
+                                                                              "ham"
+                                                                              "cheese"
+                                                                              "habanero"]
+                                                                       :type "string"}
+                                                               :type "array"}}
+                                       :required ["name" "toppings"]
+                                       :additionalProperties false}}}
+
+      (fact "spec is valid"
+        (v/validate spec) => nil))))
diff --git a/test-suites/compojure1/test/compojure/api/test_domain.clj b/test-suites/compojure1/test/compojure/api/test_domain.clj
new file mode 100644
index 00000000..cd9eee65
--- /dev/null
+++ b/test-suites/compojure1/test/compojure/api/test_domain.clj
@@ -0,0 +1,17 @@
+(ns compojure.api.test-domain
+  (:require [schema.core :as s]
+            [compojure.api.sweet :refer :all]
+            [ring.util.http-response :refer [ok]]))
+
+(s/defschema Topping {:name s/Str})
+(s/defschema Pizza {:toppings (s/maybe [Topping])})
+
+(s/defschema Beef {:name s/Str})
+(s/defschema Burger {:ingredients (s/maybe [Beef])})
+
+(def burger-routes
+  (routes
+    (POST "/burger" []
+      :return Burger
+      :body [burger Burger]
+      (ok burger))))
diff --git a/test-suites/compojure1/test/compojure/api/test_utils.clj b/test-suites/compojure1/test/compojure/api/test_utils.clj
new file mode 100644
index 00000000..60d24a87
--- /dev/null
+++ b/test-suites/compojure1/test/compojure/api/test_utils.clj
@@ -0,0 +1,103 @@
+(ns compojure.api.test-utils
+  (:require [cheshire.core :as cheshire]
+            [clojure.string :as str]
+            [peridot.core :as p]
+            [clojure.java.io :as io]
+            [compojure.api.routes :as routes])
+  (:import [java.io InputStream]))
+
+(defn read-body [body]
+  (if (instance? InputStream body)
+    (slurp body)
+    body))
+
+(defn parse-body [body]
+  (let [body (read-body body)
+        body (if (instance? String body)
+               (cheshire/parse-string body true)
+               body)]
+    body))
+
+(defn extract-schema-name [ref-str]
+  (last (str/split ref-str #"/")))
+
+(defn find-definition [spec ref]
+  (let [schema-name (keyword (extract-schema-name ref))]
+    (get-in spec [:definitions schema-name])))
+
+;;
+;; integration tests
+;;
+
+;;
+;; common
+;;
+
+(defn json [x] (cheshire/generate-string x))
+
+(defn json-stream [x] (io/input-stream (.getBytes (json x))))
+
+(defn follow-redirect [state]
+  (if (some-> state :response :headers (get "Location"))
+    (p/follow-redirect state)
+    state))
+
+(defn raw-get* [app uri & [params headers]]
+  (let [{{:keys [status body headers]} :response}
+        (-> (p/session app)
+            (p/request uri
+                       :request-method :get
+                       :params (or params {})
+                       :headers (or headers {}))
+            follow-redirect)]
+    [status (read-body body) headers]))
+
+(defn get* [app uri & [params headers]]
+  (let [[status body headers]
+        (raw-get* app uri params headers)]
+    [status (parse-body body) headers]))
+
+(defn form-post* [app uri params]
+  (let [{{:keys [status body]} :response}
+        (-> (p/session app)
+            (p/request uri
+                       :request-method :post
+                       :params params))]
+    [status (parse-body body)]))
+
+(defn raw-post* [app uri & [data content-type headers]]
+  (let [{{:keys [status body]} :response}
+        (-> (p/session app)
+            (p/request uri
+                       :request-method :post
+                       :headers (or headers {})
+                       :content-type (or content-type "application/json")
+                       :body (.getBytes data)))]
+    [status (read-body body)]))
+
+(defn post* [app uri & [data]]
+  (let [[status body] (raw-post* app uri data)]
+    [status (parse-body body)]))
+
+(defn headers-post* [app uri headers]
+  (let [[status body] (raw-post* app uri "" nil headers)]
+    [status (parse-body body)]))
+
+;;
+;; get-spec
+;;
+
+(defn extract-paths [app]
+  (-> app routes/get-routes routes/ring-swagger-paths :paths))
+
+(defn get-spec [app]
+  (let [[status spec] (get* app "/swagger.json" {})]
+    (assert (= status 200))
+    (if (:paths spec)
+      (update-in spec [:paths] (fn [paths]
+                                 (into
+                                   (empty paths)
+                                   (for [[k v] paths]
+                                     [(if (= k (keyword "/"))
+                                        "/" (str "/" (name k))) v]))))
+      spec)))
diff --git a/test/compojure/api/integration_test.clj b/test/compojure/api/integration_test.clj
index 29c46384..e8a022da 100644
--- a/test/compojure/api/integration_test.clj
+++ b/test/compojure/api/integration_test.clj
@@ -535,7 +535,107 @@
         (is (= pertti body)))
       (is (= 1 @execution-times)))))
 
-(deftest swagger-docs-test
+(deftest ring-middleware-format-swagger-docs
+  (let [app (api
+              {:format {:formats [:json-kw :edn :UNKNOWN]}}
+              (swagger-routes)
+              (GET "/user" []
+                (continue)))]
+
+    (testing "api-listing shows produces & consumes for known types"
+      (is (= (get-spec app)
+             {:swagger "2.0"
+              :info {:title "Swagger API"
+                     :version "0.0.1"}
+              :basePath "/"
+              :consumes ["application/json" "application/edn"]
+              :produces ["application/json" "application/edn"]
+              :definitions {}
+              :paths {"/user" {:get {:responses {:default {:description ""}}}}}}))))
+
+  (testing "swagger-routes"
+
+    (testing "with defaults"
+      (let [app (api (swagger-routes))]
+
+        (testing "api-docs are mounted to /"
+          (let [[status body] (raw-get* app "/")]
+            (is (= 200 status))
+            (is (str/includes? body "<title>Swagger UI</title>"))))
+
+        (testing "spec is mounted to /swagger.json"
+          (let [[status body] (get* app "/swagger.json")]
+            (is (= 200 status))
+            (is (= "2.0" (:swagger body)))))))
+
+    (testing "with partial overridden values"
+      (let [app (api (swagger-routes {:ui "/api-docs"
+                                      :data {:info {:title "Kikka"}
+                                             :paths {"/ping" {:get {}}}}}))]
+
+        (testing "api-docs are mounted"
+          (let [[status body] (raw-get* app "/api-docs")]
+            (is (= 200 status))
+            (is (str/includes? body "<title>Swagger UI</title>"))))
+
+        (testing "spec is mounted to /swagger.json"
+          (let [[status body] (get* app "/swagger.json")]
+            (is (= 200 status))
+            (is (= "2.0" (:swagger body)))
+            (is (= "Kikka" (-> body :info :title)))
+            (is (some? (-> body :paths (get (keyword "/ping"))))))))))
+
+  (testing "swagger via api-options"
+
+    (testing "with defaults"
+      (let [app (api)]
+
+        (testing "api-docs are not mounted"
+          (let [[status body] (raw-get* app "/")]
+            (is (nil? status))))
+
+        (testing "spec is not mounted"
+          (let [[status body] (get* app "/swagger.json")]
+            (is (nil? status))))))
+
+    (testing "with spec"
+      (let [app (api {:swagger {:spec "/swagger.json"}})]
+
+        (testing "api-docs are not mounted"
+          (let [[status body] (raw-get* app "/")]
+            (is (nil? status))))
+
+        (testing "spec is mounted to /swagger.json"
+          (let [[status body] (get* app "/swagger.json")]
+            (is (nil? status))
+            (is (= "2.0" (:swagger body))))))))
+
+  (testing "with ui"
+    (let [app (api {:swagger {:ui "/api-docs"}})]
+
+      (testing "api-docs are mounted"
+        (let [[status body] (raw-get* app "/api-docs")]
+          (is-200-status status)
+          (is (str/includes? body "<title>Swagger UI</title>"))))
+
+      (testing "spec is not mounted"
+        (let [[status body] (get* app "/swagger.json")]
+          (is (nil? status))))))
+
+  (testing "with ui and spec"
+    (let [app (api {:swagger {:spec "/swagger.json", :ui "/api-docs"}})]
+
+      (testing "api-docs are mounted"
+        (let [[status body] (raw-get* app "/api-docs")]
+          (is-200-status status)
+          (is (str/includes? body "<title>Swagger UI</title>"))))
+
+      (testing "spec is mounted to /swagger.json"
+        (let [[status body] (get* app "/swagger.json")]
+          (is-200-status status)
+          (is (= "2.0" (:swagger body))))))))
+
+(deftest muuntaja-swagger-docs-test
   (let [app (api
               {:formats (m/select-formats
                           m/default-options
diff --git a/test19/compojure/api/coercion/spec_coercion_test.clj b/test19/compojure/api/coercion/spec_coercion_test.clj
index 53412eac..b19c0762 100644
--- a/test19/compojure/api/coercion/spec_coercion_test.clj
+++ b/test19/compojure/api/coercion/spec_coercion_test.clj
@@ -393,7 +393,7 @@
                                        :responses {:default {:description ""}}}}
                         "/body-map" {:post {:parameters [{:description ""
                                                           :in "body"
-                                                          :name ""
+                                                          :name "body"
                                                           :required true
                                                           :schema {:properties {:x {:format "int64"
                                                                                     :type "integer"}
@@ -404,7 +404,7 @@
                                             :responses {:default {:description ""}}}}
                         "/body-params" {:post {:parameters [{:description ""
                                                              :in "body"
-                                                             :name ""
+                                                             :name "body"
                                                              :required true
                                                              :schema {:properties {:x {:format "int64"
                                                                                        :type "integer"}
@@ -415,7 +415,7 @@
                                                :responses {:default {:description ""}}}}
                         "/body-string" {:post {:parameters [{:description ""
                                                              :in "body"
-                                                             :name ""
+                                                             :name "body"
                                                              :required true
                                                              :schema {:type "string"}}]
                                                :responses {:default {:description ""}}}}
@@ -476,7 +476,7 @@
                                                        :default {:description ""}}}
                                      :post {:parameters [{:description ""
                                                           :in "body"
-                                                          :name ""
+                                                          :name "body"
                                                           :required true
                                                           :schema {:properties {:x {:format "int64"
                                                                                     :type "integer"}