This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Pedestal is a Clojure web framework that brings simplicity, power, and focus to server-side development. It's built around the interceptor pattern and supports asynchronous request handling, Server-Sent Events, and WebSockets as first-class citizens.
Requirements:
- Clojure 1.11 or later
- Java 17+
- Servlet API 5.0
From the tests subdirectory:
cd tests
clj -X:testThe test suite includes a custom test runner (io.pedestal.test-runner/test) to avoid test hangs. Tests require JVM options for attaching and async checking which are configured in tests/deps.edn.
Testing utilities:
- Use
io.pedestal.connector.test/response-forto test interceptor chains without starting a server - The
coerce-request-bodyprotocol handles String, File, or InputStream request bodies - Tests use
matcher-combinatorsfor assertions andmockfnfor mocking
Running specific tests:
# Run a specific namespace
cd tests
clj -M -e "(require 'io.pedestal.http.route-test) (clojure.test/run-tests 'io.pedestal.http.route-test)"
# Or use the test runner with namespace filters
clj -X:test :includes '[:io.pedestal.http]'From the root directory:
# Install all modules to local Maven repository
clj -T:build install
# Build and deploy all modules (requires clean workspace)
clj -T:build deploy-all
# Dry run (build and install locally, don't deploy)
clj -T:build deploy-all :dry-run true# Run clj-kondo on all sources
clj -T:build lint# Generate Codox documentation for all modules
clj -T:build codox
# Generate with custom output path
clj -T:build codox :output-path '"target/api-docs"'# Run CVE vulnerability checks
clj -T:nvd cve-checkThe tests/deps.edn includes aliases for experimenting with OpenTelemetry:
# Using OpenTelemetry SDK
cd tests
clj -M:otel
# Using OpenTelemetry Java Agent (requires downloading agent jar first)
clj -M:otel-agent# Advance version and commit/tag
clj -T:build advance-version :level :patch :commit true :tag true
# Valid levels: :major, :minor, :patch, :release, :snapshot, :beta, :rc, :alpha
# Update version manually
clj -T:build update-version :version '"1.0.0"'Pedestal is organized into multiple modules in dependency order:
- common - Shared utilities and environment detection
- telemetry - Metrics, tracing via OpenTelemetry
- log - SLF4J-based structured logging abstraction
- interceptor - Core interceptor execution engine
- error - Error handling utilities
- route - HTTP routing and URL generation
- service - High-level HTTP service configuration and default interceptors
- servlet - Jakarta Servlet API bridge
- jetty - Jetty 12 HTTP server implementation
- http-kit - Http-Kit server implementation
- embedded - Embedded server utilities
All modules use deps.edn for dependency management. Each module has its own deps.edn with :local/root references to other modules.
The foundation of Pedestal. An interceptor is a map with optional :enter, :leave, and :error functions:
{:name ::my-interceptor
:enter (fn [context] ...) ; Process request (forward)
:leave (fn [context] ...) ; Process response (reverse)
:error (fn [context ex] ...)} ; Handle exceptionsInterceptors receive and return a context map containing :request and :response (Ring format).
The context flows through the interceptor chain:
:request- Ring request map (from HTTP):response- Ring response map (returned as HTTP):route- Matched route information:url-for- Function to generate URLs from route names- Namespaced keys under
::io.pedestal.interceptor.chainfor internal state
- Enter Phase: Interceptors execute in order, each transforming context
- Handler: Creates the
:responsein context - Leave Phase: Interceptors execute in reverse, can modify
:response - Error Phase: If exception occurs,
:errorhandlers execute in reverse
Interceptors can return a core.async channel for asynchronous processing.
The service map (::http/... keys) configures the HTTP service:
::http/routes- Route definitions::http/router- Router type (:prefix-tree,:map-tree, etc.)::http/type- Server type (:jetty,:http-kit)::http/hostand::http/port- Bind address::http/interceptors- Custom interceptor chain- Security:
::http/allowed-origins,::http/enable-csrf,::http/secure-headers
Routes are defined separately from handlers:
- Multiple syntaxes: table-based, terse, or expanded
- Routing is itself an interceptor in the chain
- Supports path parameters, query parameters, and constraints
url-for-routesgenerates URLs from route names
Router Implementations:
:sawtooth- Default in 0.8.0+, identifies routing conflicts, prefers literal paths over parameterized ones:prefix-tree- Fast, tree-based matching:map-tree- Alternative tree implementation:linear-search- Simple linear search (for small route sets)
Sawtooth Router Benefits:
- Reports conflicting routes at startup (e.g.,
/users/searchvs/users/:id) - Prefers literal matches:
/users/searchmatches before/users/:id - More predictable routing behavior
Route Definition Formats:
Table format (recommended):
[["/users" :get users-list :route-name :list-users]
["/users/:id" :get user-detail :route-name :get-user]
["/users/:id" :put user-update :route-name :update-user]]Terse format:
["/users" {:get users-list}
["/:id" {:get user-detail
:put user-update}]]Map format:
#{[:get "/users" users-list :route-name :list-users]
[:get "/users/:id" user-detail :route-name :get-user]}HTTP Request
↓
Servlet/Connector (converts to Ring :request)
↓
Context Map created, interceptor chain enqueued
↓
Enter Phase (logging → CORS → body parsing → routing → handler)
↓
Leave Phase (response formatting → headers → logging)
↓
Convert :response to HTTP and return
Default interceptors include: tracing, logging, CORS, session handling, body parsing, routing, secure headers, static file serving, and response formatting.
The io.pedestal.connector namespace provides a cleaner API not tied to Jakarta Servlet:
(require '[io.pedestal.connector :as conn])
(-> (conn/default-connector-map "0.0.0.0" 8080)
(conn/with-default-interceptors)
(conn/with-routes routes)
conn/start!)This is the recommended approach for new applications. The default router is now :sawtooth (changed in 0.8.0).
Legacy vs Modern:
- Modern (0.8.0+):
io.pedestal.connectornamespace - simpler, not tied to Servlet API - Legacy (deprecated):
io.pedestal.httpnamespace - still works but will be removed in future - All new development should use
io.pedestal.connector
Pedestal 0.8.0+ is designed for REPL-driven development:
The with-routes macro (not function) ensures routes are re-evaluated on each request during development:
(-> connector-map
(conn/with-routes my-routes)) ; macro - routes refreshed from varThis works with clj-reload for smooth REPL workflows.
Enable dev-mode interceptors for better debugging:
(-> connector-map
conn/optionally-with-dev-mode-interceptors) ; adds dev interceptors when dev-mode? is trueDev mode is enabled via:
- System property:
-Dio.pedestal.dev-mode=true - Environment variable:
PEDESTAL_DEV_MODE=true
Use io.pedestal.connector.test/response-for to test interceptor chains:
(require '[io.pedestal.connector.test :as test])
(test/response-for service-fn
:get "/users/123"
:headers {"Accept" "application/json"})- Core protocol definitions and specs use
io.pedestal.Xnamespaces - Implementation details under
io.pedestal.X.impl - HTTP-related code under
io.pedestal.http - Test code mirrors source structure under
tests/test
- All tests are consolidated in the
tests/subdirectory - Tests reference modules via
:local/rootdependencies - Use
matcher-combinatorsfor assertions - Use
mockfnfor mocking - Test namespaces end with
-test
Naming:
- Interceptor names must be namespaced keywords:
::my-interceptor - Anonymous (unnamed) interceptors are deprecated in 0.8.0
- Functions converted to interceptors now get default names based on their class name
- To disable default handler names: set
io.pedestal.interceptor/*default-handler-names*to false
Creation:
- Use
defbefore,defafterhelpers for simple interceptors - Use
definterceptormacro to define interceptor record types (new in 0.8.0) - Interceptors can be created from functions, records, or maps
Using definterceptor:
The definterceptor macro creates a record that implements IntoInterceptor and can also work with component lifecycle:
(require '[io.pedestal.interceptor :refer [definterceptor]])
(definterceptor logging-interceptor [logger]
(enter [this context]
(log/info logger :event ::request-received)
context)
(leave [this context]
(log/info logger :event ::response-sent)
context))
;; Create instance
(def my-interceptor (->logging-interceptor my-logger))
;; Can also implement component protocols
(definterceptor database-query [db-conn]
component/Lifecycle
(start [this] ...)
(stop [this] ...)
(handle [this request]
(query-database db-conn request)))The interceptor name is automatically derived as a namespaced keyword from the record name.
The build system in build/build.clj handles all modules:
- Uses
io.github.hlship/build-toolsfor common build tasks - Coordinates building multiple interdependent modules
- Handles version updates across all
deps.ednfiles - Creates combined API documentation via Codox
- Use
io.pedestal.lognamespace, not direct SLF4J - Log statements support structured data:
(log/info :event ::my-event :data data) - MDC support via bindings
- Logback is used for tests (
ch.qos.logback/logback-classic)
- Most work happens in individual module directories
- Keep modules in dependency order when making cross-module changes
- Update tests in
tests/subdirectory - Run
clj -T:build lintbefore committing - Ensure tests pass with
cd tests && clj -X:test
- Add to the appropriate module's
deps.edn - If it's a common test dependency, add to
tests/deps.edn - Maintain version alignment across modules where dependencies are shared
- API docs generated via Codox from docstrings
- Main documentation site uses Antora (in
docs/directory) - Ensure docstrings follow standard Clojure format
- Examples in
samples/anddocs/modules/guides/examples/
Request/Response:
- Ring-compatible request/response maps throughout:
{:status :headers :body} - Request body can be String, InputStream, File, or nil
- Response body can be String, InputStream, File, ReadableByteChannel, or collections (auto-converted to JSON)
Context Threading:
- Every interceptor receives and returns the context map
- Add data to context for downstream interceptors:
(assoc context :db-conn conn) - Read from context:
(:db-conn context)
Asynchronous Processing:
- Return core.async channel from interceptor for non-blocking operations
- Channel must convey the updated context map
- Execution pauses until context is delivered
Extensibility via Protocols:
IntoInterceptor- convert custom types to interceptorsExpandableRoutes- custom route definition formatsHandler,OnEnter,OnLeave,OnError- for record-based interceptorsPedestalConnector- custom server implementations
Configuration Over Code:
- Service/connector map for declarative configuration
- Interceptor chain composition via data structures
- Route definitions as data (vectors, maps, sets)
Security First:
- Secure defaults: HTTPS, CSP headers, CSRF tokens
- CORS middleware for cross-origin requests
- Session handling with secure cookies
- Body size limits and content-type validation
Always clean your target directory when upgrading Pedestal versions:
rm -rf target/Key Breaking Changes:
- Minimum Clojure version: 1.11
- Sawtooth router is now the default (was prefix-tree)
- Anonymous interceptors are deprecated
io.pedestal.httpnamespace deprecated (useio.pedestal.connector)- Many 0.7.0 deprecated APIs removed
- WebSocket upgrade requests now go through routing (not special-cased)
- Static file serving now goes through routing (not via interceptors)
- Server-Sent Events fields terminated with
\n(was\r\n)
WebSockets:
- Now handled through regular routing (0.8.0+)
- Implement
InitializeWebSocketprotocol for WebSocket endpoints - WebSocket routes defined like any other route with
:ws trueflag
Server-Sent Events (SSE):
- Use
io.pedestal.http.ssenamespace - Return streaming response with proper SSE formatting
- SSE fields now terminated with single
\n(both\nand\r\nare valid per spec) - See
samples/server-sent-eventsfor examples
Interceptors can return core.async channels for non-blocking operations:
{:enter (fn [context]
(async/go
(let [data (async/<! (fetch-from-db))]
(assoc context :response {:status 200 :body data}))))}If you see routing conflict warnings with the Sawtooth router:
- Review conflicting paths (e.g.,
/users/searchand/users/:id) - Reorder routes or use constraints to disambiguate
- The Sawtooth router will prefer literal matches over parameterized ones
If you see deprecation warnings about anonymous interceptors:
- Add
:namekeys to all interceptor maps - Use namespace-qualified keywords:
{:name ::my-interceptor ...} - For functions, add
:namemetadata:^{:name ::handler} (fn [req] ...)
If routes aren't updating in the REPL:
- Ensure you're using the
with-routesmacro, not a function - Check that dev-mode is enabled
- Use
clj-reloadfor proper namespace reloading - Routes must be in a var for the macro to re-evaluate them
If tests hang or timeout:
- Tests use custom runner in
tests/dev/io.pedestal.test-runner - Ensure JVM options in
tests/deps.ednare applied - Check for unclosed core.async channels in tests
When making changes across modules:
- Modules have
:local/rootdependencies on each other - Install to local Maven repo with
clj -T:build installafter cross-module changes - Respect module dependency order (see Architecture section)
- Documentation: https://pedestal.io
- Support: #pedestal on Clojurians Slack
- Contributing Guide: https://pedestal.io/pedestal/0.8/contributing.html
- Sample Applications: See
samples/directory for examples of WebSockets, SSE, metrics, CORS, etc.