This document describes the architecture and design of the Sandbar system.
Sandbar is a Clojure web service built on three core technologies:
┌─────────────────────────────────────────────────────────────┐
│ HTTP Clients │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Pedestal HTTP Server │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ Routes │─▶│Interceptors │─▶│ Handlers │ │
│ └─────────────┘ └─────────────┘ └─────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Service Layer │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ Datatype │ │ Endpoint │ │ Content │ │
│ │ API │ │ Macros │ │ Negotiation │ │
│ └─────────────┘ └─────────────┘ └─────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Database Layer │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Datomic Peer │ │
│ │ ┌───────────┐ ┌───────────┐ ┌───────────────┐ │ │
│ │ │ Schema │ │ Query │ │ Transactions │ │ │
│ │ └───────────┘ └───────────┘ └───────────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
The application uses Stuart Sierra's Component library for managing stateful resources with proper startup/shutdown ordering.
(defn make-system [config]
(component/system-map
:datomic (datomic/make-datomic (:db-uri config))
:pedestal (component/using
(pedestal/make-pedestal (:http-port config))
[:datomic])
:nrepl (nrepl/make-nrepl (:nrepl-port config))))
sandbar.db.datomic
Manages the Datomic Peer connection:
*conn* dynamic varsandbar.server.pedestal
Manages the HTTP server:
sandbar.service.routessandbar.server.nrepl
Manages the network REPL server:
;; In sandbar.core
(defn go []
(init)
(start))
(defn stop []
(when-let [sys @system]
(component/stop sys)
(reset! system nil)))
Routes are defined in sandbar.service.routes using Pedestal's terse routing syntax:
(def routes
`[[["/" {:get home-page}
^:interceptors [(body-params/body-params (content/body-parsers))
params/url-decode-path-params]
["/api"
^:interceptors [content/data-body
content/accept-content
params/parsed-params
params/validated-params]
["/status" {:get status/status-handler}]
["/store"
["/classes" {:get store/list-classes}
["/:ns/:name" {:get store/get-class}
["/slots" {:get store/list-slots}]
...]]
...]]]]])
Requests flow through a chain of interceptors:
| Interceptor | Purpose |
|---|---|
body-params | Parse request body (JSON, EDN, form data) |
url-decode-path-params | Decode URL-encoded path parameters |
data-body | Set up response body serialization |
accept-content | Content negotiation (JSON, EDN, Transit) |
parsed-params | Merge and parse all parameters |
validated-params | Validate parameters against schema |
log-params | Log request parameters (development) |
log-response | Log response details (development) |
Handlers are defined using the defhandler macro:
(defhandler get-class
"GET /api/store/classes/:ns/:name - Full class description"
[request db-conn {:keys [ns name]}]
(let [class-kw (keyword ns name)]
(if-let [desc (describe-entity class-kw)]
{:class class-kw
:description desc
:slots (dt/slots-of class-kw)
...}
(endpoint/not-found {:error "Class not found"}))))
The macro provides:
Schema files are stored in schema/*.edn:
;; schema/user.edn
[
[{:db/id #db/id[:db.part/user -1]
:db/ident :model/User}]
[{:db/ident :user/login
:db/valueType :db.type/string
:dt/domain :model/User
:dt/type :dt/Property
...}]
[{:db/ident :model/User
:dt/type :dt/Class
:dt/subclass-of :dt/Ref
:dt/slots [:user/uuid :user/login :user/secret]}]
]
The sandbar.db.datatype namespace provides the metamodel API:
| Function | Description |
|---|---|
all-classes | List all class idents |
all-properties | List all property idents |
class-of | Get the class of an entity |
parents-of | Get direct parent classes |
ancestors-of | Get all ancestor classes |
subclasses-of | Get all subclasses (transitive) |
slots-of | Get all effective slots (inherited + direct) |
instance-of? | Check if entity is instance of class |
subclass-of? | Check if class is subclass of another |
make | Create a validated typed instance |
validate | Validate an entity against its class |
Configuration is stored in config/config.edn:
{:http-port 8080
:nrepl-port 28888
:db-uri "datomic:dev://localhost:4334/sandbar"
:required-schema [:meta :literal :ref :fn :any :user :twit]}
Accessed via sandbar.util.edn:
(edn/config-value :http-port) ;; => 8080
(edn/config-value :db-uri) ;; => "datomic:dev://..."
src/sandbar/
├── core.clj # System lifecycle (go, stop, init, start)
├── sys.clj # Global system state
│
├── api/
│ ├── status.clj # Health check endpoint
│ └── store.clj # Metamodel REST API
│
├── db/
│ ├── datomic.clj # Datomic connection component
│ ├── datatype.clj # Metamodel query API
│ ├── fn.clj # Database functions
│ └── rules.clj # Datalog rules
│
├── server/
│ ├── nrepl.clj # nREPL server component
│ └── pedestal.clj # HTTP server component
│
├── service/
│ ├── config.clj # Pedestal service configuration
│ ├── content.clj # Content negotiation interceptors
│ ├── endpoint.clj # Handler macros
│ ├── params.clj # Parameter validation
│ └── routes.clj # Route definitions
│
└── util/
├── codec.clj # Encoding utilities
├── common.clj # Common utilities
├── diff.clj # Data diffing
├── edn.clj # EDN/config loading
└── http_status.clj # HTTP status codes
schema/myclass.edn:required-schema in config/config.edn;; schema/myclass.edn
[
[{:db/ident :model/MyClass
:dt/type :dt/Class
:dt/subclass-of :dt/Ref
:dt/context "model"
:dt/label "MyClass"
:dt/slots [:myclass/name :myclass/value]}]
[{:db/ident :myclass/name
:db/valueType :db.type/string
:dt/type :dt/Property
:dt/domain :model/MyClass
:dt/range :db.type/string
:db/cardinality :db.cardinality/one}]
]
src/sandbar/api/src/sandbar/service/routes.clj;; In api/myapi.clj
(defvalidator ::my-handler [_] identity)
(defhandler my-handler
"GET /api/my-endpoint"
[_ _ params]
{:result "success"})
;; In service/routes.clj
["/my-endpoint" {:get myapi/my-handler}]
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |