Review Clojure and ClojureScript code changes for compliance with Metabase coding standards, style violations, and code quality issues...
This guide covers Clojure and ClojureScript coding conventions for Metabase. See also: CLOJURE_STYLE_GUIDE.adoc for the Community Clojure Style Guide.
General Naming:
acc, i, pred, coll, n, s, k, fkebab-case for all variables, functions, and constantsFunction Naming:
age not calculate-age or get-age)!Destructuring:
snake_case keysDocstrings:
src or enterprise/backend/src must have docstring[[other-var]] not backticksComments:
TODO format: ;; TODO (Name M/D/YY) -- descriptionVisibility:
^:private unless it is used elsewheredeclare (put public functions near the end)Size and Structure:
let/cond)Keywords and Metadata:
:query-type/normal not :normal:arglists metadata if they're functions but wouldn't otherwise have itOrganization:
deftest forms for logically separate test cases-test or -test-<number>Performance:
^:parallelOSS Modules:
metabase.<module>.* patternsrc/metabase/<module>/Enterprise Modules:
metabase-enterprise.<module>.* patternenterprise/backend/src/metabase_enterprise/<module>/Module Structure:
<module>.api or <module>.api.* namespaces<module>.core using Potemkin imports<module>.models.*<module>.settings<module>.schemaModule Linters:
:clj-kondo/ignore [:metabase/modules]Required Elements:
:- <schema> after route string)Naming Conventions:
snake_case/api/dashboard/:id)Behavior:
GET endpoints should not have side effects (except analytics)defendpoint forms should be small wrappers around Toucan model codeRestrictions:
lib, lib-be, or query-processor modulesNaming:
snake_case identifiersBest Practices:
t2/select-one-fn instead of fetching entire rows for one columnDocumentation:
docs/developers-guide/driver-changelog.mdImplementation:
driver argument to other driver multimethodsread-column-thunk in JDBC-based driversExamples:
Linter Suppressions:
#_:clj-kondo/ignore (keyword form)Configurable Options:
:internal defsetting instead./bin/mage kondo-updated master (or whatever target branch)./bin/mage kondo <file or files>./bin/mage kondo-updated HEAD./bin/mage cljfmt-files [path]./bin/mage run-tests namespace/test-name./bin/mage run-tests namespace./bin/mage run-tests test/metabase/notification Because the module lives in that directory.Note: the ./bin/mage run-tests command accepts multiple args, so you can pass
./bin/mage run-tests namespace/test-name namespace/other-test namespace/third-test
to run 3 tests, or
./bin/mage run-tests test/metabase/module1 test/metabase/module2 to run 2 modules.
./bin/mage -check-readable <file> [line-number]Note: If you have
clojure-mcptools available (check for tools likeclojure_eval), always prefer those over./bin/mage -repl. The MCP tools provide better integration, richer feedback, and avoid shell escaping issues. Only use./bin/mage -replas a fallback when clojure-mcp is not available.
./bin/mage -repl '<code>'./bin/mage -repl '(+ 1 1)' where (+ 1 1) is your Clojure code../bin/mage -repl -h for more details.To call your.namespace/your-function on arg1 and arg2:
./bin/mage -repl --namespace your.namespace '(your-function arg1 arg2)'
DO NOT use "require", "load-file" etc in the code string argument.
The ./bin/mage -repl command returns three separate, independent outputs:
value: The return value of the last expression (best for data structures)stdout: Any printed output from println etc. (best for messages)stderr: Any error messages (best for warnings and errors)Example call:
./bin/mage -repl '(println "Hello, world!") '\''({0 1, 1 3, 2 0, 3 2} {0 2, 1 0, 2 3, 3 1})'
Example response:
ns: user
session: 32a35206-871c-4553-9bc9-f49491173d1c
value: ({0 1, 1 3, 2 0, 3 2} {0 2, 1 0, 2 3, 3 1})
stdout: Hello, world!
stderr:
For effective REPL usage:
println for human-readable messagesWhat to flag:
CLOJURE_STYLE_GUIDE.adoc exists in the working directory, also check compliance with the community Clojure style guideWhat NOT to post:
Example bad code review comments to avoid:
This TODO comment is properly formatted with author and date - nice work!
Good addition of limit 1 to the query - this makes the test more efficient without changing its behavior.
The kondo ignore comment is appropriately placed here
Test name properly ends with -test as required by the style guide.
Special cases:
Use this to scan through changes efficiently:
tbl, zs')kebab-case for all variables and functions!src or enterprise/backend/src have useful docstrings[[other-var]] not backticksTODO comments include author and date: ;; TODO (Name 1/1/25) -- description^:private unless used elsewheredeclare when avoidable (public functions near end)let/cond)deftest forms for distinct test cases^:parallel-test or -test-<number>metabase.<module>.*, EE: metabase-enterprise.<module>.*)<module>.api namespaces<module>.core with Potemkin:clj-kondo/ignore [:metabase/modules]:- <schema>)snake_case/api/dashboard/:id)GET has no side effects (except analytics)lib, lib-be, or query-processor modulest2/select-one-fn instead of selecting full rows for one columndocs/developers-guide/driver-changelog.mddriver argument to other driver methods (no hardcoded driver names)read-column-thunk#_:clj-kondo/ignore keyword form)Quick scan for common issues:
| Pattern | Issue |
|---|---|
calculate-age, get-user |
Pure functions should be nouns: age, user |
update-db, save-model |
Missing ! for side effects: update-db!, save-model! |
snake_case_var |
Should use kebab-case |
| Public var without docstring | Add docstring explaining purpose |
;; TODO fix this |
Missing author/date: ;; TODO (Name 1/1/25) -- description |
(defn foo ...) in namespace used elsewhere |
Should be (defn ^:private foo ...) |
| Function > 20 lines | Consider breaking up into smaller functions |
/api/dashboards/:id |
Use singular: /api/dashboard/:id |
Query params with snake_case |
Use kebab-case for query params |
| New API endpoint without tests | Add tests for the endpoint |
For style violations:
This pure function should be named as a noun describing its return value. Consider
userinstead ofget-user.
For missing documentation:
This public var needs a docstring explaining its purpose, inputs, and outputs.
For organization issues:
This function is only used in this namespace, so it should be marked
^:private.
For API conventions:
Query parameters should use kebab-case. Change
user_idtouser-id.