{"id":23902673,"url":"https://github.com/mtumilowicz/clojure-ring-reitit-h2-workshop","last_synced_at":"2025-06-22T17:35:08.724Z","repository":{"id":110875200,"uuid":"469148981","full_name":"mtumilowicz/clojure-ring-reitit-h2-workshop","owner":"mtumilowicz","description":"Exemplary Clojure webapp written with ring, reitit, conman.","archived":false,"fork":false,"pushed_at":"2024-12-15T18:17:27.000Z","size":124,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-02-23T10:44:18.686Z","etag":null,"topics":["clojure","clojure-development","clojure-exercises","clojure-rest","clojure-tutorial","conman","reitit","ring","tutorial","workshop","workshop-materials"],"latest_commit_sha":null,"homepage":"","language":"Clojure","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mtumilowicz.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-03-12T17:13:36.000Z","updated_at":"2024-12-15T18:16:59.000Z","dependencies_parsed_at":null,"dependency_job_id":"6081cd7b-0ba7-4126-8fc4-bcfe7b01724f","html_url":"https://github.com/mtumilowicz/clojure-ring-reitit-h2-workshop","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/mtumilowicz/clojure-ring-reitit-h2-workshop","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mtumilowicz%2Fclojure-ring-reitit-h2-workshop","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mtumilowicz%2Fclojure-ring-reitit-h2-workshop/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mtumilowicz%2Fclojure-ring-reitit-h2-workshop/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mtumilowicz%2Fclojure-ring-reitit-h2-workshop/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mtumilowicz","download_url":"https://codeload.github.com/mtumilowicz/clojure-ring-reitit-h2-workshop/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mtumilowicz%2Fclojure-ring-reitit-h2-workshop/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":261335039,"owners_count":23143458,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["clojure","clojure-development","clojure-exercises","clojure-rest","clojure-tutorial","conman","reitit","ring","tutorial","workshop","workshop-materials"],"created_at":"2025-01-04T22:49:46.445Z","updated_at":"2025-06-22T17:35:03.680Z","avatar_url":"https://github.com/mtumilowicz.png","language":"Clojure","funding_links":[],"categories":[],"sub_categories":[],"readme":"[![Build Status](https://app.travis-ci.com/mtumilowicz/clojure-ring-reitit-h2-workshop.svg?branch=master)](https://app.travis-ci.com/mtumilowicz/clojure-ring-reitit-h2-workshop)\n[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)\n\n# clojure-ring-reitit-h2-workshop\n* references\n    * https://cemerick.com/blog/2011/07/05/flowchart-for-choosing-the-right-clojure-type-definition-form.html\n    * https://github.com/omniti-labs/jsend\n    * https://stackoverflow.com/questions/10947636/idiomatic-way-to-represent-sum-type-either-a-b-in-clojure\n    * https://www.metosin.fi/blog/schema-spec-web-devs/\n    * http://funcool.github.io/struct/latest/\n    * https://www.manning.com/books/clojure-in-action\n    * https://pragprog.com/titles/dswdcloj3/web-development-with-clojure-third-edition/\n    * https://pragprog.com/titles/shcloj3/programming-clojure-third-edition/\n    * https://www.manning.com/books/the-joy-of-clojure-second-edition\n    * https://pragprog.com/titles/vmclojeco/clojure-applied/\n    * https://clojuredocs.org/clojure.core\n    * https://github.com/ring-clojure/ring\n    * https://metosin.github.io/muuntaja\n    * https://github.com/metosin/reitit\n    * https://github.com/technomancy/leiningen/blob/stable/doc/TUTORIAL.md\n    * https://github.com/technomancy/leiningen/blob/stable/sample.project.clj\n    * https://stackoverflow.com/questions/5459865/how-can-i-throw-an-exception-in-clojure\n    * https://stackoverflow.com/questions/7658981/how-to-reload-a-clojure-file-in-repl\n    * https://clojure.org/guides/destructuring\n    * https://www.braveclojure.com/multimethods-records-protocols\n    * https://ilanuzan.medium.com/functional-polymorphism-using-clojures-multimethods-825c6f3666e6\n    * https://ilanuzan.medium.com/polymorphism-w-clojure-protocols-396ff472ff3c\n    * https://ericnormand.me/mini-guide/deftype-vs-defrecord\n    * https://stackoverflow.com/questions/13150568/deftype-vs-defrecord\n    * https://stackoverflow.com/questions/37048167/what-is-the-difference-between-macroexpand-and-macroexpand-1-in-clojure\n    * http://funcool.github.io/struct/latest/\n    * https://github.com/clojure/core.match\n    * https://github.com/tolitius/cprop\n    * https://github.com/luminus-framework/conman\n    * https://github.com/layerware/hugsql\n    * https://github.com/tolitius/mount\n    * https://medium.com/pragmatic-programmers/use-hugsql-c3cf85e53678\n    * https://github.com/edn-format/edn\n\n## preface\n* it may be worthwhile to refer first (basics)\n    * https://github.com/mtumilowicz/clojure-concurrency-stm-workshop\n* goals of this workshop\n    * introduction into clojure web development: ring, reitit\n    * introduction to validation with struct\n    * show how to integrate with relational db: conman, mount, hugsql\n    * advanced clojure features: threading, polymorphism, macros, pattern matching\n    * modeling domain with records\n    * practicing destructuring\n* note that in this project we have standard dependency injection (map with dependencies passed\nas a first param)\n    * components would be subject to different workshops\n* workshop plan\n    * add PATCH method - to edit parts of person data\n\n## macros\n* most distinguishing feature of Clojure when compared to Java etc\n* Clojure runtime processes source code differently compared to other languages\n    1. read phase: reader converts a stream of characters (the source code) into Clojure data structures\n    1. evaluation phase: data structures are evaluated to execute the program\n    * Clojure offers a hook between the two phases\n        * allow code to be modified programmatically before evaluation\n* hello world example\n    * similar tradition when it comes to explaining macros\n        * add `unless` control structure to the language\n    * without macros it is impossible\n        ```\n        (defn unless [test then]\n            (if (not test)\n            then))\n        ```\n        * all functions execute according to the following rules\n            * evaluate all arguments passed to the function call form\n            * evaluate the function using the values of the arguments\n        * remark: you can pass thunk (function) instead of then to delay evaluation (it changes sematics)\n            ```\n            (defn unless [test then]\n                (if (not test)\n                (then)))\n            ```\n    * with macros\n        ```\n        (defmacro unless [test then]\n            `(if (not ~test)\n            ~then))\n        ```\n* syntax\n    * templating: backquote (`)\n    * inserting value: ~\n* verifying macros\n    * `macroexpand-1`\n        * if form represents a macro form, returns its expansion, else returns form\n    * `macroexpand`\n        * repeatedly calls `macroexpand-1` on form until it no longer represents a macro form, then returns it\n    * `macroexpand` vs `macroexpand-1`\n        ```\n        (defmacro inner-macro [arg]\n          `(println ~arg))\n\n        (defmacro top-level-macro [arg]\n          `(inner-macro ~arg))\n\n        (macroexpand-1 '(inner-macro \"hello\")) // (clojure.core/println \"hello\")\n        (macroexpand-1 '(top-level-macro \"hello\")) // (user/inner-macro \"hello\")\n        (macroexpand '(top-level-macro \"hello\")) // (clojure.core/println \"hello\")\n        ```\n* macros in syntax: `when`, `when-not`, `cond`, `if-not`, etc\n\n## polymorphism\n* two approaches: multimethods and protocols\n* multimethod\n    * example\n        ```\n        (defmulti make-sound :type)\n        (defmethod make-sound :Dog [x] \"Woof Woof\")\n        (defmethod make-sound :Cat [x] \"Miauuu\")\n\n        (make-sound {:type :Dog}) =\u003e \"Woof Woof\"\n        (make-sound {:type :Cat}) =\u003e \"Miauuu\"\n        ```\n    * `defmulti`: name, signature and dispatch function\n        * note that dispatch function can be any function\n            ```\n            (def QUICK-SORT-THRESHOLD 5)\n\n            (defmulti my-sort (fn [arr]\n                                (if (every? integer? arr)\n                                   :counting-sort\n                                   (if (\u003c (count arr) QUICK-SORT-THRESHOLD)\n                                      :quick-sort\n                                      :merge-sort))))\n            ```\n            * in an OOP language this behavior can’t be implemented in a Polymorphic way\n                * it would have to be a code with an if statement\n    * `defmethod`: function implementation for a particular dispatch value\n    * default case: if no method is associated with the dispatching value, the multimethod will\n    look for a method associated with the default dispatching value (:default), and\n    will use that if present\n* protocols\n    * replace what in an OOP language we know as interfaces\n    * example\n        ```\n        (defprotocol Shape\n          (area [this])\n          (perimeter [this]))\n        (defrecord Rectangle [width length]\n          Shape\n          (area [_] (* width length))\n          (perimeter [_] (+ (* 2 width) (* 2 length))))\n        (defrecord Square [side]\n          Shape\n          (area [_] (* side side))\n          (perimeter [_] (* 4 side)))\n\n        (def square (-\u003eSquare 4))\n        (def rectangle (-\u003eRectangle 2 5))\n\n        (map area [square rectangle])   // (16 10)\n        (map :side [square rectangle])  // (4 nil)\n        (map :length [square rectangle])     // (nil 5)\n        ```\n* protocols are usually preferred for type-based dispatch\n    * have the ability to group related functions together in a single protocol\n\n## syntax\n* threading\n    * example\n        ```\n        (- (/ (+ c 3) 2) 1) // difficult to read, because it’s written inside out\n        ```\n        vs\n        ```\n        (-\u003e c (+ 3) (/ 2) (- 1))\n        ```\n    * thread first: `-\u003e`\n        * takes the first argument supplied and place it in the second position of the next expression\n        * example of how a macro can manipulate code to make it easier to read\n            * something like this is nearly impossible in most other languages\n        * useful for pulling nested data structures\n            ```\n            (-\u003e person :employer :address :city)\n            ```\n            vs\n            ```\n            (:city (:address (:employer person)))\n            ```\n    * thread last: `-\u003e\u003e`\n        * takes the first expression and moving it into the last place\n        * common use case: working with sequences + using map, reduce and filter\n            * these functions accepts the sequence as the last element\n    * two related ones: `some-\u003e` and `some-\u003e\u003e`\n        * computation ends if the result of any step in the expansion is `nil`\n* records\n    * custom, maplike data types (associate keys with values)\n    * example\n        ```\n        (defrecord Person [fname lname address])\n        (defrecord Address [street city state zip])\n\n        (def stu\n            (map-\u003ePlanet {:fname \"Stu\"\n                          :lname \"Halloway\"\n                          :address (map-Address {\n                                :street \"200 N Mangum\"\n                                :city \"Durham\"\n                                :state \"NC\"\n                                :zip 27701})\n                          })) // factory methods map-EntityName; expects map\n        ```\n    * most of the time records are a better choice for domain entities than Map\n    * internals\n        * dynamically generate compiled bytecode for a named class with a set of given fields\n        * fields can have type hints, and can be primitive\n        * provides a complete implementation of a persistent map\n            * value-based equality and hashCode\n            * associative support\n            * keyword accessors for fields\n            * extensible fields (you can assoc keys not supplied with the defrecord definition)\n        * deftype vs defrecord\n            * deftype creates a bare-bones object which implements a protocol\n            * defrecord creates an immutable persistent map which implements a protocol\n* destructuring\n    * is a way to concisely bind names to the values inside a data structure\n    * is broken up into two categories\n        * sequential destructuring\n            ```\n            (let [[x y z] my-vector]\n              (println x y z))\n            ```\n            * if the vector is too small, the extra symbols will be bound to nil\n        * associative destructuring\n            ```\n            (defn greet-user [person]\n                  (let [first (:first-name person)\n                        last (:last-name person)]\n                  (println \"Welcome,\" first last)))\n            ```\n            * we can destructure it in method declaration\n                ```\n                (defn greet-user [{first :first-name last :last-name}]\n                      (println \"Welcome,\" first last)))\n                ```\n            * `:or` - supply a default value if the key is not present\n                ```\n                (defn greet-user [{first :first-name last :last-name} :or {:first \"unknown\" :last \"unknown\"}]\n                      (println \"Welcome,\" first last)))\n                ```\n            * `:as` - binds whole argument\n                ```\n                (defn greet-user [{first :first-name last :last-name} :as person]\n                      (println \"Welcome,\" first last)))\n                ```\n            * `:keys` - if local bindings and keys are the same\n                ```\n                (defn greet-user [{:keys [first-name last-name]}]\n                      (println \"Welcome,\" first-name last-name))\n                ```\n* pattern matching\n    * adds pattern matching support to the Clojure programming language\n    * examples\n        * vector\n            ```\n            (let [x [1 2 3]]\n              (match [x]\n                [[_ _ 2]] :a0\n                [[1 1 3]] :a1\n                [[1 2 3]] :a2\n                :else :a3))\n            ```\n        * map\n            ```\n            (let [x {:a 1 :b 1}]\n              (match [x]\n                [{:a _ :b 2}] :a0\n                [{:a 1 :b 1}] :a1\n                [{:c 3 :d _ :e 4}] :a2\n                :else nil))\n            ```\n        * types that we not control\n            * define accessors\n                ```\n                (extend-type java.util.Date\n                  clojure.core.match.protocols/IMatchLookup\n                  (val-at [this k not-found]\n                    (case k\n                      :day (.getDay this)\n                      :month (.getMonth this)\n                      :year (.getYear this)\n                      not-found)))\n                ```\n            * and then pattern match\n                ```\n                (match [(java.util.Date. 2010 10 1 12 30)]\n                   [{:year 2009 :month a}] a\n                   [{:year (:or 2010 2011) :month b}] b\n                   :else :no-match)\n                ```\n\n## validation\n* using: https://github.com/funcool/struct\n    * structural validation library\n* features\n    * no macros: validators are defined using plain data.\n    * dependent validators: the ability to access to already validated data\n    * coercion: the ability to coerce incoming values to other types\n    * no exceptions: no exceptions used in the validation process\n* by default\n    * all validators are optional\n        * to make the value mandatory, you should use a specific required validator\n    * additional entries in the map are not stripped\n        * `(st/validate +scheme+ {:strip true})`\n* example\n    ```\n    (require '[struct.core :as st])\n\n    (def MoviePremierScheme\n      {:name [st/required st/string]\n       :year [st/required st/number]})\n\n    (-\u003e {:name \"Blood of Elves\" :year 1994}\n        (st/validate MoviePremierScheme)) // [nil {:name \"Blood of Elves\" :year 1994}]\n\n    (-\u003e {:year \"1994\"}\n        (st/validate MoviePremierScheme)) // [{:name \"this field is mandatory\", :year \"must be a number\"} {}]\n    ```\n    * then we could pattern match\n* support for nested data structures\n    ```\n    (def PersonScheme\n      {[:personal-details :born-year] st/integer\n       [:address :street] st/string})\n    ```\n* custom messages\n    ```\n    (def schema\n      {:age [[st/in-range 18 26 :message \"The age must be between %s and %s\"]]})\n    ```\n    * wildcards will be replaced by args of validator\n* coercions\n    ```\n    (def schema\n      {:year [[st/integer :coerce str]]})\n\n    (def schema {:year [st/required st/integer-str] // predefined coercions\n                 :id [st/required st/uuid-str]})\n    ```\n\n## mount\n* how to manage state in application\n    * either use components or mount\n    * components vs mount\n        * framework vs lib\n        * if a managing state library requires a whole app buy-in - it is a framework\n            * dependency graph is usually quite large and complex\n                * it has everything (every piece of the application) in it\n        * if stateful things are kept lean and low level (i.e. I/O, queues, threads, connections, etc.),\n        dependency graphs are simple and small\n            * everything else is just namespaces and functions: the way it should be\n* provides a defstate macro that allows us to declare something which can be started and stopped\n    * example: database connection, a thread-pool, or an HTTP server\n        * sometimes referred to as resources\n    * we provide :start and :stop keys\n        * specify the code that should run when the resource is started and stopped, respectively\n    * once a resource is started, the return value of the :start function is bound to the symbol we used in our defstate\n* example\n    * defining\n        ```\n        (require '[mount.core :refer [defstate]])\n        (defstate conn :start (create-conn))\n        ```\n    * using\n        ```\n        (ns app\n          (:require [above :refer [conn]]))\n        ```\n* to make the application state enjoyably reloadable\n    * mount has start and stop functions that will walk all the states created\n    with defstate and start / stop them accordingly\n    * reloading with REPL\n        ```\n        (mount/stop)\n        (mount/start)\n        ```\n* dependencies are \"injected\" by requiring on the namespace level\n    * mount trusts the Clojure compiler to maintain the start and stop order for all the defstates\n\n## conman\n* luminus database connection management and SQL query generation library\n* provides pooled connections using the HikariCP library\n* queries are generated using HugSQL and wrapped with connection aware functions\n* HugSql\n    * can find any SQL file in your classpath\n    * example\n        * first, define in `users.sql` file:\n            ```\n            -- :name add-user! :! :n\n            -- :doc  adds a new user\n            INSERT INTO users\n            (id, pass)\n            VALUES (:id, :pass)\n            ```\n        * then\n            ```\n            (hugsql/def-db-fns \"users.sql\")\n            (add-user! db {:id \"hug\" :pass \"sql\"})\n            ```\n            * function accepts the database connection as its first parameter, followed by the query map\n                * keys in the map have the same names as those we defined earlier in the `users.sql` file\n    * uses specially formatted SQL comments as metadata for defining functions\n        * `:name` - name of the function\n        * `:execute` or `:!` — can be used for any statement\n            * indicates that the function modifies the data\n        * `:query` or `:?` — indicates a query with a result set\n        * `:returning-execute` or `:\u003c!` — is used to indicate an INSERT … RETURNING\n        * `:insert` or `:i!` — attempts to return the generated keys\n        * return hints\n            * `:one` or `:1` — a result with a single row\n            * `:many` or `:*` — a result with multiple rows\n            * `:affected` or `:n` — the number of affected rows\n            * `:raw` — the result generated by the underlying database adapter\n        * placeholder keys for the VALUES\n            * HugSQL uses these keys to look up the parameters in the input map when the generated function is called\n* queries are bound to the connection using the `bind-connection` macro\n    * macro accepts the connection var followed by one or more strings representing SQL query files\n    * example\n        ```\n        (conman/bind-connection *db* \"sql/queries.sql\")\n        ```\n    * `bind-connection` generates functions from sql in the current namespace\n* `connect!` function should be called to initialize the database connection\n    ```\n    (defstate ^:dynamic *db*\n              :start (conman/connect! {:jdbc-url (env :database-url)})\n              :stop (conman/disconnect! *db*))\n    ```\n*  it's possible to use the `with-transaction` macro to rebind it to the transaction connection\n\n## config\n* cprop.core\n* loads an EDN config from a classpath and/or file system and merges it with system properties and ENV variables\n    * returns an (immutable) map\n        * working with a config is no different than just working with a map\n    * edn digression\n        * extensible data notation\n        * used by Datomic and other applications as a data transfer format\n        * includes keywords, symbols, strings, numbers, lists, sets, vectors, and maps\n        * tags are the core differentiator\n            * reason why it's called Extensible Data Notation\n            * # character allows the subsequent form to be parsed in a special way\n            * example\n                * #uuid tag converts a string representation of a UUID into the environment’s\n                underlying UUID implementation (e.g. java.util.UUID )\n* example\n    * definition\n        ```\n        {:datomic\n            {:url \"datomic:sql://?jdbc:postgresql://localhost:5432/datomic?user=datomic\u0026password=datomic\"}\n         :source\n            {:account\n                {:rabbit\n                   {:host \"127.0.0.1\"\n                    :port 5672\n                    :vhost \"/z-broker\"\n                    :username \"guest\"\n                    :password \"guest\"}}}\n         :answer 42}\n        ```\n    * loading\n        ```\n        (def conf (load-config))\n\n        (conf :answer) // 42\n        ```\n* by default cprop would look in two places for configuration files:\n    * classpath: for the config.edn resource\n    * file system: for a path identified by the conf system property\n\n## ring\n* is a Clojure web applications library\n* higher level frameworks such as Compojure or lib-noir use Ring as a common basis\n    * good to have a basic understanding of Ring\n* supports synchronous and asynchronous endpoints and middleware\n    * we focus here only on synchronous part\n* four components:\n    * handler\n        * **synchronous** handlers\n            ```\n            (defn what-is-my-ip [request]\n              {:status 200\n               :headers {\"Content-Type\" \"text/plain\"}\n               :body (:remote-addr request)})\n            ```\n    * request\n        * represented by Clojure maps\n        * some standard keys: `:headers`, `:body`, `:content-type`, `:path-params`\n    * response\n        * created by the handler\n        * contains three keys: `:status`, `:headers`, `:body`\n    * middleware\n        * higher-level functions\n        * adds additional functionality to handlers\n        * first argument of a middleware function should be a handler\n        * its return value should be a new handler function\n        * threading macro (-\u003e) can be used to chain middleware together\n            ```\n            (def app\n              (-\u003e handler\n                  (wrap-content-type \"text/html\")\n                  (wrap-keyword-params)\n                  (wrap-params)))\n            ```\n        * muuntaja/wrap-formats\n            * negotiates a request body based on accept, accept-charset and content-type headers\n                * decodes the body with an attached Muuntaja instance into `:body-params`\n            * encodes also the response body\n        * query params\n            * required: `[app.gateway.middleware :refer [wrap-params]`\n                * adds three new keys to the request map:\n                    * `:query-params` - map of parameters from the query string\n                    * `:form-params` - map of parameters from submitted form data\n                    * `:params` - merged map of all parameters\n                * example\n                    * `:query-string \"q=clojure\"` is transformed into `:query-params {\"q\" \"clojure\"}`\n\n## reitit\n* fast data-driven router for Clojure\n* routes are defined as vectors\n    * [String path and optional (non-sequential) route argument]\n* paths can have path-parameters (:id) or catch-all-parameters (*path)\n* example\n    * define routes\n        ```\n        [\"/api\"\n         [\"/admin\" {:middleware [middleware-wrappers...]}\n          [\"\" admin-handler]\n          [\"/db\" db-handler]]\n         [\"/ping\" ping-handler]]\n        ```\n    * and create router\n        ```\n        (def router\n          (r/router routes))\n        ```\n* coercion\n    * process of transforming parameters (and responses) from one format into another\n    * by default, all wildcard and catch-all parameters are parsed into strings\n    * problem\n        ```\n        (def router\n          (r/router\n            [\"/:company/users/:user-id\" ::user-view])) // :path-params {:company \"metosin\", :user-id \"123\"},\n        ```\n        but we would like to treat user-id as a number, so\n        ```\n        (def router\n          (r/router\n            [\"/:company/users/:user-id\" {:name ::user-view // {:path {:company \"metosin\", :user-id 123}}\n                                         :coercion reitit.coercion.schema/coercion\n                                         :parameters {:path {:company s/Str\n                                                             :user-id s/Int}}}]\n            {:compile coercion/compile-request-coercers}))\n        ```\n* default handlers\n    ```\n    (reitit/create-default-handler\n          {:not-found\n           (constantly (response/not-found \"404 - Page not found\"))\n           :method-not-allowed\n           (constantly (response/method-not-allowed \"405 - Not allowed\"))\n           :not-acceptable\n           (constantly (response/not-acceptable \"406 - Not acceptable\"))})))\n    ```\n* exception handling\n    * good practice\n        * add `{:exception pretty/exception}`\n        * it makes routing errors (for example conflicting route) increases readability of errors\n        * only for exceptions thrown in router creation\n* middleware\n    * debugging\n        * `reitit.ring.middleware.dev/print-request-diffs`\n            * prints a request and response diff between each middleware\n    * custom param decoders\n        * example\n            ```\n            (def new-muuntaja\n              (m/create\n               (-\u003e m/default-options\n                   (assoc-in [:formats \"application/json\" :decoder-opts :bigdecimals] true)\n                   (assoc-in [:formats \"application/json\" :encoder-opts :date-format] \"yyyy-MM-dd\"))))\n            ```","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmtumilowicz%2Fclojure-ring-reitit-h2-workshop","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmtumilowicz%2Fclojure-ring-reitit-h2-workshop","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmtumilowicz%2Fclojure-ring-reitit-h2-workshop/lists"}