CLAUDE.md - Clojure/ClojureScript SaaS Project Guidelines

Build Commands

  • npm run dev - Start development environment (Shadow-CLJS + Tailwind)

  • bb dev - Start both frontend and backend servers for development

  • bb release - Full release build for production

  • bb build-icons - Generate SVG icon sprite

Lint & Format Commands

  • npm run standard-clj - Lint Clojure/ClojureScript files

  • npm run standard-clj -- fix path/to/file.clj - Auto-fix formatting issues

  • clj-kondo --lint src/ - Run clj-kondo linter on source directory

  • Prettier is used for JS/CSS formatting

  • Code is auto-formatted with standard-clj on pre-commit via Lefthook

Test Commands

  • ALWAYS run clj-kondo --report-level error --lint your.namespace after editing a namespace and immediately fix any issue.

  • Tests located in /test/clj/saas/ and /test/cljs/

  • Run Clojure tests: clj -X:test-clj

  • Run ClojureScript tests: clj -M:test-cljs

  • Run single test: clj -X:test :only namespace/test-name

Coding Style Guidelines

  • Naming: Use kebab-case for namespaces, functions, variables (Clojure standard)

  • Organization: /src/clj (server), /src/cljc (shared), /src/cljs (client)

  • State Management: Re-frame for frontend state, documented in ADRs

  • UI: UIx for React components, Tailwind CSS with DaisyUI for styling

  • Error Handling: Use middleware with standard exception types

    • :system.exception/internal (500), :system.exception/business (400), etc.

Architecture

  • Full-stack Clojure/ClojureScript app with shadow-cljs, Tailwind + DaisyUI

  • Single-page application architecture with server-rendered landing page

  • Authentication uses access token (15m, in-memory) + refresh token (90d) pattern

  • See docs/authentication.md for detailed token rotation approach

  • UI component documentation in /portfolio/ directory with interactive stories

WebSocket Communication

  • Uses Sente library for WebSocket connections (taoensso.sente)

  • Client-side connection setup in /src/cljs/saas/ws.cljs

  • Server handlers defined in /src/clj/saas/web/routes/websocket.clj

  • Authentication via JWT tokens:

    • Pass tokens in :params map with Sente connection (not in headers)

    • Server extracts user ID from JWT claims via req->user-id function

  • Message format: [:event-id payload] (e.g., [:ai-chat/message {:message "Hello"}])

  • Handler multimethod pattern:

    (defmethod ws/handle-message :event-id
      [{:keys [?data]}]
      ;; Handle message here
    )

UI Components

  • DaisyUI components wrapped in UIx React components at /src/cljc/saas/common/ui/core.cljc

  • Component examples in /portfolio/src/saas/components/ directory

  • Key component properties:

    • Most components take standard HTML attributes like :class, :style

    • Special props like :color accept keywords (:primary, :error, etc.)

    • Size modifiers (:xs, :sm, :md, :lg) for many components

    • Boolean flags like bordered? follow Clojure's convention with ? suffix

  • Page structure pattern:

    • Define a main page component with defui

    • Initialize state with dispatched events in component body

  • Docs for DaisyUI here

  • Docs for UIX here

  • When writing tailwind classes as keywords, when you have more comples classes, such as modifiers, result to using string classes as oposed to keywords. The same if you need to use custom values. Use keyword based class names only for simple classes.

    ($ :div.md:bg-red-200.h-[100px])  ;; bad
    ($ :div {:class "md:bg-red-200 h-[100px]"}) ;; good
    
    ($ :div#cool-id) ;; bad
    ($ :div {:id "cool-id"}) ;; good

Portfolio Component Documentation

  • Always use the defscene macro to create component examples

  • Required namespace import: [portfolio.react-18 :as portfolio :refer-macros [defscene]]

  • Include portfolio/configure-scenes with a title at the top of each scenes file

  • Basic example:

    (ns saas.components.example-scenes
      (:require
       [portfolio.react-18 :as portfolio :refer-macros [defscene]]
       [saas.common.ui.core :as ui]
       [saas.common.ui.utils :refer [cn]]
       [uix.core :refer [$ defui]]))
    
    (portfolio/configure-scenes
      {:title "Example Scenes"})
    
    (defui scene-container [{:keys [children class]}]
      ($ :div {:class (cn "flex flex-col gap-3" class)} children))
    
    (defscene basic-example
      :title "Basic Example"
      ($ ui/button "Click me"))
  • Add component scenes file to portfolio.cljs to make it available

  • Use scene-container component for consistent layout across scenes

Re-frame Conventions

  • Always define event/subscription parameters as a map, not positional arguments:

    ;; CORRECT: Use map destructuring for parameters
    (rf/reg-sub
      :form/field-state
      (fn [db [_ {:keys [form-id field-id]}]]
        ;; Access parameters by name
        (get-in db [:forms form-id :values field-id])))
    
    ;; INCORRECT: Don't use positional arguments
    (rf/reg-sub
      :form/field-state
      (fn [db [_ form-id field-id]]
        ;; Positional parameters are harder to maintain
        (get-in db [:forms form-id :values field-id])))
  • This improves maintainability, readability, and allows adding parameters without breaking existing code

Re-frame with UIx

  • For dispatching events and other re-frame operations, continue using re-frame.core:

    (require [re-frame.core :as rf])
    (rf/dispatch [::action-id payload]) ;; Still use rf/dispatch
    (rf/reg-event-db ::event-id handler-fn) ;; Still use rf for registration
  • For subscribing to re-frame state in UIx components, use use-subscribe:

    ;; INCORRECT in UIx components - won't trigger re-renders properly
    (let [data @(rf/subscribe [::sub-id])] ...)
    
    ;; CORRECT - properly reactive with UIx
    (require [uix.re-frame :refer [use-subscribe]])
    (let [data (use-subscribe [::sub-id])] ...)
  • Only subscriptions need the special use-subscribe hook in UIx components

  • This ensures proper React integration for reactive updates when state changes

Routes and API Structure

  • Different route files are separated by middleware requirements, not by functionality:

    • routes/api.clj - For most API routes with standard middleware

    • routes/webhooks.clj - For webhook endpoints that need to handle raw payloads

    • routes/pages.clj - For server-rendered pages

  • Always add new API endpoints to routes/api.clj unless there's a specific middleware conflict

  • Group related endpoints under a common parent path (e.g., /api/payments/...)

  • Webhook endpoints are used for external service callbacks (e.g., Stripe)

Reitit/Malli Integration

  • Upgraded to Reitit 0.7.2 requires updated Malli schema formats

  • Schema format changes:

    • Replace function predicates (like string?) with keywords (:string)

    • Use vector schemas (like [:string]) instead of bare predicates

    • Path parameters should use [:map [:param-name :type]] format

    • Never use nil as a schema value, use empty map [:map] instead

  • Main routes defined in /src/cljc/saas/common/routes.cljc

  • Route names in /src/cljc/saas/routes.cljc

  • Define request/response schemas in common/schema.cljc to be shared across frontend and backend

Last updated