Liking cljdoc? Tell your friends :D

Your First Module

After running bb scaffold generate, a complete module is created. This page walks through each generated file.

Generated structure

For a product module with name, sku, and price fields:

libs/product/
├── src/product/
│   ├── core/
│   │   └── product.clj      ← Pure business logic
│   ├── shell/
│   │   ├── persistence.clj  ← Database operations
│   │   ├── service.clj      ← Service orchestration
│   │   ├── http.clj         ← HTTP handlers
│   │   └── module-wiring.clj
│   ├── ports.clj            ← Protocol definitions
│   └── schema.clj           ← Malli validation schemas
├── test/product/
│   ├── core/product_test.clj          ← Unit tests
│   ├── shell/service_test.clj         ← Integration tests
│   └── shell/persistence_test.clj     ← Contract tests
└── resources/boundary/product/
    └── migrations/001-create-products.sql

The schema

schema.clj defines the shape of a Product entity using Malli:

(def Product
  [:map
   [:id         :uuid]
   [:name       [:string {:min 1 :max 200}]]
   [:sku        [:string {:min 1 :max 50}]]
   [:price      [:decimal {:min 0}]]
   [:created-at inst?]
   [:updated-at inst?]])

(def ProductInput
  [:map
   [:name       [:string {:min 1 :max 200}]]
   [:sku        [:string {:min 1 :max 50}]]
   [:price      [:decimal {:min 0}]]])
All keys are kebab-case internally. Conversion to snake_case (DB) or camelCase (API) happens only at the boundary. See Conventions.

The core

core/product.clj contains only pure functions:

(ns boundary.product.core.product
  (:require [boundary.product.schema :as schema]
            [boundary.core.utils.validation :as v]))

(defn prepare-product
  "Validates input and returns a product entity ready for persistence."
  [input]
  (let [entity (merge {:id (random-uuid)
                       :created-at (java.time.Instant/now)
                       :updated-at (java.time.Instant/now)}
                      input)]
    (v/validate-with-transform schema/Product entity)))

No I/O. No side effects. Easy to test without mocks.

The ports

ports.clj defines the interface between layers:

(ns boundary.product.ports)

(defprotocol IProductRepository
  (create-product! [this product])
  (find-product-by-id [this id])
  (list-products [this opts])
  (update-product! [this id changes])
  (delete-product! [this id]))

(defprotocol IProductService
  (create-product [this input])
  (get-product [this id])
  (list-products [this opts]))

The service

shell/service.clj orchestrates validation, core logic, and persistence:

(defn create-product [this input]
  (let [[valid? errors data] (validate ProductInput input)]
    (if valid?
      (let [product (product-core/prepare-product data)]
        (persistence/create-product! (:repo this) product))
      (throw (ex-info "Validation failed"
                      {:type :validation-error :errors errors})))))

Run the module’s tests

clojure -M:test:db/h2 :product

Next steps

  • Validation — using Malli schemas and the validation framework

  • Authentication — protecting your endpoints

  • Functional Core / Imperative Shell — deeper dive into the pattern

Can you improve this documentation?Edit on GitHub

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
Move to previous article
Move to next article
Ctrl+/Jump to the search field
× close