{"id":21196703,"url":"https://github.com/nomnom-insights/nomnom.duckula","last_synced_at":"2025-07-10T05:30:50.816Z","repository":{"id":45646571,"uuid":"216409306","full_name":"nomnom-insights/nomnom.duckula","owner":"nomnom-insights","description":"🦆🧛🕸Framework for building HTTP APIs with Clojure, JSON and Avro","archived":false,"fork":false,"pushed_at":"2023-03-02T22:13:08.000Z","size":97,"stargazers_count":16,"open_issues_count":2,"forks_count":1,"subscribers_count":6,"default_branch":"master","last_synced_at":"2025-07-04T00:08:11.562Z","etag":null,"topics":["clojure","framework","http","rpc"],"latest_commit_sha":null,"homepage":"","language":"Clojure","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/nomnom-insights.png","metadata":{"files":{"readme":"README.md","changelog":null,"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":"2019-10-20T18:38:43.000Z","updated_at":"2024-11-09T14:26:38.000Z","dependencies_parsed_at":"2024-11-20T19:51:24.304Z","dependency_job_id":null,"html_url":"https://github.com/nomnom-insights/nomnom.duckula","commit_stats":{"total_commits":81,"total_committers":3,"mean_commits":27.0,"dds":"0.13580246913580252","last_synced_commit":"a6c8ee965da5717c5909bfbda5745be7a287d2ce"},"previous_names":[],"tags_count":8,"template":false,"template_full_name":null,"purl":"pkg:github/nomnom-insights/nomnom.duckula","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nomnom-insights%2Fnomnom.duckula","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nomnom-insights%2Fnomnom.duckula/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nomnom-insights%2Fnomnom.duckula/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nomnom-insights%2Fnomnom.duckula/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/nomnom-insights","download_url":"https://codeload.github.com/nomnom-insights/nomnom.duckula/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nomnom-insights%2Fnomnom.duckula/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":264535989,"owners_count":23624404,"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","framework","http","rpc"],"created_at":"2024-11-20T19:41:03.926Z","updated_at":"2025-07-10T05:30:50.563Z","avatar_url":"https://github.com/nomnom-insights.png","language":"Clojure","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Duckula\n\n\nStatus\n[![CircleCI](https://circleci.com/gh/nomnom-insights/nomnom.duckula.svg?style=svg)](https://circleci.com/gh/nomnom-insights/nomnom.duckula)\n\nInstallation:\n\n[![Clojars Project](https://img.shields.io/clojars/v/nomnom/duckula.svg)](https://clojars.org/nomnom/duckula)\n\n\u003cimg src=\"https://vignette.wikia.nocookie.net/danmacgregor/images/4/4e/Count_Duckula.jpg/revision/latest?cb=20100807200739\" align=\"right\" height=\"160px\" /\u003e\n\n\u003e :warning: While Duckula is used in production by [EnjoyHQ](https://getenjoyhq.com) there are still things we're working out. You have been warned! :warning:\n\n\u003e :warning: If you value **stable** software - wait for the v1, otherwise - here be ducks\n\nDuckula is a synchronous equivalent of [Bunnicula](https://github.com/nomnom-insights/nomnom.bunnicula) bult on top of ring, HTTP, JSON and Avro:\n\n- uses Stuart Sierra's [Component](https://github.com/stuartsierra/component) for dependency injection\n- establishes conventions for synchronous HTTP APIs:\n  - HTTP POST **only**\n  - JSON for input/output (for now, full Avro support is planned)\n  - routes/URIs map to operations (e.g. `POST documents/get-by-id` instead of `GET /documents`), meaning there's no route params\n- handlers are functions receiving the request map, along with dependent components\n- validates inputs and outputs via Avro schemas\n  - supports merging multiple Avro schemas to make it easy to share definitions between endpoints and requests/responses\n- uses protocols to inject a monitoring middleware. We provide our own, which reports metrics to Statsd and errors to Rollbar - see [duckula.monitoring](https://github.com/nomnom-insights/nomnom.duckula.monitoring)\n- convention over configuration, where it makes sense\n- can generate Swagger (OpenAPI) documentation\n\n\n## Roadmap\n\n- [ ] can talk Avro (input and output) via content type negotiation\n- [ ] clj-http middleware for building type-safe clients\n\n## Rationale\n\nBased on [our experience of building a Clojure framework for RabbitMQ](https://blog.getenjoyhq.com/bunnicula-asynchronous-messaging-with-rabbitmq-for-clojure/) we learned a good deal about building a *mostly-Clojure* backend which works as a part of a system built using other languages. If your stack is 100% Clojure, Duckula might not be for you. The reason for using Avro and strongly typed validation on *the edges* of the system, rather than Spec or Schema allows us to share schemas with Javascript and Ruby clients and guarantee correctness of inputs/outputs across service boundaries.\n\nWhile we looked at solutions such as gRPC or GraphQL, neither of them had a good support for our existing tooling, required adopting a completely different approach/tooling/etc or would need a significant effort to migrate. Duckula offers a compromise between using known (to us!) stack, simplicity and is based on our previous attempts at building *frameworks* in Clojure.\n\nBy using JSON and HTTP, we can leverage standard tooling such as nginx, curl and `jq`. By using Avro, we get a simple solution for defining schemas *at runtime* and support for multiple languages, not only Clojure. Lack of a compilation step is a huge benefit to the developer productivity.\n\n## Usage\n\nDuckula is mostly config driven. An example config for a \"test-rpc-service\" would be:\n\n```clojure\n\n(def config\n  {:name \"some-rpc-service\"\n   :mangle-names? false ;; default false, see below\n   :endpoints { \"/search/test\" {:request [\"shared/Tag\" \"search/test/Request\"] ; re-use schemas\n                                :response [\"shared/Tag\" \"search/test/Response\"]\n                                :handler handler.search/handler} ; request handler\n               \"/number/multiply\" {:request \"number/multiply/Request\"\n                                   :response \"number/multiply/Response\"\n                                   :soft-validate? true ; default false, see below\n                                   :handler handler.number/handler}\n               ;; no validation\n               \"/echo\" {:handler handler.echo/handler}}})\n\n```\n\nThen in your Component system:\n\n```clojure\n\n(def system-map\n  (merge\n   {:db (some.db/connection)\n    ;; required for metrics and error reporting\n    :monitoring duckula.component.basic-monitoring/basic}\n   ;; see dev-resources dir for a working example\n   ;; at the very least, your ring middleware stack needs to handle\n   ;; JSON parsing from the POST body\n   ;; also, Duckula assumes that Components are included in the :component\n   ;; key of the request map\n   ;; You can use duckula.middleware/with-monitoring middleware for that\n   (duckula.test.component.http-server/create\n     (duckula.middleware/wrap-handler\n      (duckula.handler/build config))\n    [:db :monitoring]\n    {:port 3000 :name \"api\"})))\n\n```\n\n(You can see an example web server component example in `dev-resources/duckula/test/component/http-server.clj`)\n\nDuckula will:\n\n\n- only match endpoints listed under `:endpoints` key\n- lookup schemas for each endpoint and use them to validate incoming POST body and response body, note that schemas are optional - you can use Duckula as a simplistic route  with metrics and error reporting built-in\n- request handler function will receive the full request map, along with component dependencies\n- when serving requests it will track:\n  - request time (for `/search/test` it would record latency under `some-rpc-service.search.test`)\n  - number of successfully handled requests under `some-rpc-service.search.test.success`\n  - number of errored (invalid input etc) handled requests under `some-rpc-service.search.test.error`\n  - number of failed (exceptions) handled requests under `some-rpc-service.search.test.failure`\n- log/report exceptions (if any)\n- when schemas fail to validate it responds with standard error response and info about which schema and when it failed\n- if a route doesn't exist it will respond with standard 404 and record metrics\n\n### `mangle-names?`\n\nBy default all map keys and enum values have to use `_` (underscore) as word separators. That's true for inputs (POST data) and outputs (JSON responses). That also means, that all keys with `-` dashes in key names, will be replaced with `_` underscores. See more info about schema mangling here: https://github.com/nomnom-insights/abracad#basic-deserialization\n\nIf you want to enable automatic conversion of underscores to dashes (and make underscored names invalid) set `mangle-names?` to true.\n\nSince `mangle-names?` is a bit cryptic, you can use:\n\n- `snake-case-names?` set to true as an alias for `mangle-names? false` (the default)\n- `kebab-case-names?` set to true as an alias for `mangle-names? true`\n\n\n#### Example\n\n```json\n {\n  \"name\" : \"Request\",\n  \"fields\" : [\n  {\n      \"name\" : \"order_by\",\n      \"type\" : {\n        \"name\" : \"OrderBy\",\n        \"type\" : \"enum\",\n        \"symbols\" : [\n          \"created_at\",\n          \"updated_at\"\n        ]\n      }\n    }\n  ]\n}\n```\n\nWhen `mangle-names?` is set to false (**default**) the following payload is *valid*: `{order_by:  \"updated_at\"}` .\n\nWhen `mangle-names?` is set to true the example payload would be **invalid** and this would be required: `{order-by: \"updated-at\"}`\n\n### `soft-validate?`\n\nWhen set to true Duckula will perform input and output validation, but **will still pass request and response data** to/from the request handler function even if it's not conforming to the given schema. Use case for that is adding a schema to an existing endpoint or rolling out changes to the existing schema, but only to see if there's any invalid data being sent in/out, without affecting actual request processing.\n**Note** - this means that your handler functions still have to deal with potentially invalid input, as you might receive request body which is not correct!\n\n\n### Schema loading and merging\n\nYou can pass a resource path to a single schema, and it will be looked up in *resource* paths, with the `schema/endpoint` prefix.\n\nExample: `search/get/Request` will be resolved to `schema/endpoint/search/get/Request.avsc`.\nYou can configure the endpoints to reuse schemas, by merging them in order:\n\n```clojure\n\n{ :endpoints { \"/test\"  { :request [\"shared/Tag\" \"shared/User\" \"test/Request\" ]\n                          :response [\"shared/Tag\" \"shared/User\" \"test/Response\" ]\n                          :handler test-fn } } }\n```\n\n# Monitoring\n\nThe only hard dependency is the monitoring component, which implements `duckula.protcol/Monitoring` protocol. A sample implementation can be found in `duckula.component.monitoring` namespace.\n\nWe have a complete, production grade implementation based on [Caliban](https://github.com/nomnom-insights/nomnom.caliban) for reporting exceptions to Rollbar, and [Stature](https://github.com/nomnom-insights/nomnom.stature) for recording metrics to a Statsd server.\n\nSee it here: https://github.com/nomnom-insights/nomnom.duckula.monitoring\n\n# Usage\n\n### As a standalone handler in a web server\n\n```clojure\n(ns duckula.server\n  \"Test HTTP server\"\n  (:require [duckula.test.component.http-server :as http-server]\n            duckula.handler\n            duckula.middleware\n            [duckula.component.basic-monitoring :as monitoring]\n            [duckula.handler.echo :as handler.echo]\n            [duckula.handler.number :as handler.number]\n            [duckula.handler.search :as handler.search]\n            [com.stuartsierra.component :as component]))\n\n(def server (atom nil))\n\n;; Assumptions:\n;; Avro schemas exist somewhere in  CLASSPATH, under schema/endpoint/ directory\n;; So here 'search/test/Response' is looked up in `schema/endpoint/search/test/Response.avsc`\n;; If rquest and/or response keys are nil, then we default  to `identity` as the validation function\n;; meaning, there's no validation :-)\n(def config\n  {:name \"some-rpc-service\"\n   :endpoints {\"/search/test\" {:request \"search/test/Request\"\n                              :response \"search/test/Response\"\n                              :handler handler.search/handler}\n   \"/number/multiply\" {:request \"number/multiply/Request\"\n                       :response \"number/multiply/Response\"\n                       :handler handler.number/handler}\n   ;; no validation\n   \"/echo\" {:handler handler.echo/handler}}})\n\n(defn start! []\n  (let [sys (component/map-\u003eSystemMap\n             (merge\n              {:monitoring monitoring/basic}\n              (http-server/create (duckula.middleare/wrap-handler (duckula.handler/build config))\n                                  [:monitoring]\n                                  {:name \"test-rpc-server\"\n                                   :port 3003})))]\n    (reset! server (component/start sys))))\n\n(defn stop! []\n  (swap! server component/stop))\n```\n\n### As part of an existing ring application\n\nAn example of how to add Duckula powered routes to an existing Compojure-based app:\n\n```clojure\n\n(def config\n  {:endpoints { \"/search\" {:request \"groups/search/Request\"\n                         :response \"groups/search/Response\"\n                         :handler service.http.handler.groups/search}\n               \"/create\" {:request \"groups/create/Request\"\n                          :response \"groups/create/Response\"\n                          :handler service.http.handler.groups/create}\n               \"/ping\" {:handler service.http.handler.groups/ping}}\n   :name \"groups-rpc\"\n   :prefix \"/groups\" ; Must match Compojure context below\n   })\n\n\n;; assumes we're using compojure\n\n(defroutes all\n  (context \"/groups\" [] (duckula.middleware/wrap-handler (duckula.handler/build config)))\n  (context \"/dashboards\" [] service.http.handlers.dashboards/routes))\n\n\n```\n\n## Swagger  \u003csup\u003ebeta\u003c/sup\u003e\n\nDuckula can generate [Swagger](https://swagger.io) JSON definition and serve the Swagger UI.\n\nTo get started, swap how your API handler is built from:\n\n```clojure\n\n(def api (duckula.middleware/wrap-handler (duckula.handler/build config)))\n```\n\n\nto\n\n```clojure\n\n(def api (duckula.middleware/wrap-handler (duckula.swagger/with-docs config)))\n```\n\nAnd restart your server.\n\nThe UI is now accessible under `/~docs/ui` and the API definition can be downloaded from `/~docs/swagger.json`\n\n\n# Changelog\n\n## [0.7.3] - 2021-12-02\n\n- Updated dependencies\n\n## [0.7.2] - 2021-07-12\n\n- Catch Throwable in request handler (not just Exception)\n\n## [0.7.1] - 2021-06-10\n\n- Adds [Swagger](https://swagger.io) support, allows for defining inline Avro schemas in the API config and ships witha minimal Ring middleware for handling JSON requests.\n- Fixes JSON content type handling\n- More clear options for disabling/enabling keyword mangling\n- **Potential breaking change** basic monitoring component implementation is now a record and provides a default instance under `duckula.component.basic-monitoring/basic`\n- Set of helper Ring middlewares for no-config setup:\n  - `duckula.middleware/wrap-handler` which provides proper JSON input/output handling\n  - `duckula.middleware/with-monitoring` - allows for using Duckula with Components, it can either inject basic monitoring layer, or accepts your own implementation of the `duckula.protocol/Monitoring`\n\n## [0.5.3] - 2020-03-25\n\nFix to response status reprting and misplaced doc string\n\n## [0.5.2] - 2020-02-15\n\nAvro schema memoization has been removed, improves dev workflow.\n\n## [0.5.1] - 2019-12-26\n\nBug fix release - fixes an issue with metrics reporting for namespaced routes.\n\n## [0.5.0] - 2019-10-23\n\nInitial public release\n\n# Authors\n\n\u003csup\u003eIn alphabetical order\u003c/sup\u003e\n\n- [Afonso Tsukamoto](https://github.com/AfonsoTsukamoto)\n- [Łukasz Korecki](https://github.com/lukaszkorecki)\n- [Marketa Adamova](https://github.com/MarketaAdamova)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnomnom-insights%2Fnomnom.duckula","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnomnom-insights%2Fnomnom.duckula","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnomnom-insights%2Fnomnom.duckula/lists"}