Liking cljdoc? Tell your friends :D

Embedded Template

Pedestal includes a template that can be used with deps-new, a tool used to generate new projects from a template.

deps-new works with the clojure (or clj) tool, and generates a deps.edn-based project. If you are a Leiningen user, it is relative straight forward to create an equivalent project.clj from the generated deps.edn.

The embedded part indicates that the template is configured to work using reference:jetty.adoc, and it starts Jetty from within a running Clojure application (an alternate, supported, but far less common approach is to bundle a Pedestal application into a WAR and deploy into Jetty, or another servlet container).

Setting up deps-new

deps-new operates as a Clojure tool, and can be added using the following command:

clojure -Ttools install-latest :lib io.github.seancorfield/deps-new :as new

You will also need some scaffolding in your ~/.clojure/deps.edn; add the following to the :aliases map:

:1.12 {:override-deps {org.clojure/clojure {:mvn/version "1.12.0"}}}
This step is only necessary if Clojure 1.12 is not your default; for most users, Clojure 1.12 will be the default. Check by running the clojure command; it will tell you your default Clojure version.

Creating a project

Before you begin, you should decide on a group name and project name for your new Pedestal application. These are combined with a slash to form the full project name.

For example, you might choose com.blueant as your group name, and peripheral as you project name (we’ll use this example below), in which case, your full project name is com.blueant/peripheral.

deps-new will create a new project in a subdirectory matching your project name: peripheral.

The command for this is somewhat arcane:

clojure -A:1.12 -Tnew create :template \
  io.github.pedestal/pedestal%embedded%io.pedestal/embedded#{libs_version} \
  :name com.blueant/peripheral
The -A:1.12 option references the global alias you set up earlier and, again, is only needed if Clojure 1.12 is not your default. Clojure 1.12 is only needed to initially construct your project; to later build or run your project, Clojure 1.10 or above is all that’s needed.

Example:

$ clojure -A:1.12 -Tnew create :template io.github.pedestal/pedestal%embedded%io.pedestal/embedded#{libs_version} :name com.blueant/peripheral

Resolving io.github.pedestal/pedestal as a git dependency
Creating project from io.pedestal/embedded in peripheral
$ tree peripheral
├── CHANGELOG.md
├── LICENSE
├── README.md
├── build.clj
├── deps.edn
├── dev
│   ├── com
│   │   └── blueant
│   │       └── peripheral
│   │           └── telemetry_test_init.clj
│   ├── dev.clj
│   └── user.clj
├── doc
│   └── intro.md
├── resources
│   ├── logback.xml
│   ├── pedestal-config.edn
│   └── public
│       └── index.html
├── src
│   └── com
│       └── blueant
│           └── peripheral
│               ├── connector.clj
│               ├── main.clj
│               ├── routes.clj
│               └── telemetry_init.clj
├── start-jaeger.sh
├── test
│   └── com
│       └── blueant
│           └── peripheral
│               └── connector_test.clj
└── test-resources
    ├── logback-test.xml
    └── pedestal-test-config.edn

17 directories, 20 files
>
The exact set of files created may change over time, as the embedded template evolves.

Exploring the new project

From the new directory (peripheral) you can run tests:

$ clj -T:build test

Running tests in #{"test"}

Testing com.blueant.peripheral.service-test

Ran 3 tests containing 3 assertions.
0 failures, 0 errors.
>

You can also fire up a REPL and start the service:

$ clj -A:test
Clojure 1.12.0
user=> (use 'dev)
nil
user=> (go!)
Routing table:
┏━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Method ┃  Path  ┃                 Name                 ┃
┣━━━━━━━━╋━━━━━━━━╋━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
┃   :get ┃ /hello ┃ :com.blueant.peripheral.routes/hello ┃
┃  :post ┃ /hello ┃ :com.blueant.peripheral.routes/greet ┃
┗━━━━━━━━┻━━━━━━━━┻━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
#object[io.pedestal.http.http_kit$create_connector$reify__17907 0x6e3f108c "io.pedestal.http.http_kit$create_connector$reify__17907@6e3f108c"]
user=>

From another window, you can open http://localhost:8080/index.html, to see a brief welcoming page.

The dev namespace provides the functions go!, start!, and stop!.

The :test alias sets up the classpath so that the dev namespace is available, and enables REPL oriented development mode, including the output of the routing table as the service started.

Because the application is running in debug mode, Pedestal has enabled extra logging output about the execution of each interceptor, and how the interceptor changed the context map.

DEBUG io.pedestal.interceptor.chain.debug - {:interceptor :io.pedestal.http.cors/dev-allow-origin, :stage :enter, :execution-id 1, :context-changes {:added {[:request :headers "origin"] ""}}, :line 128}
DEBUG io.pedestal.interceptor.chain.debug - {:interceptor :io.pedestal.http.tracing/tracing, :stage :enter, :execution-id 1, :context-changes {:added {[:bindings] ..., [:io.pedestal.http.tracing/otel-context-cleanup] ..., [:io.pedestal.http.tracing/prior-otel-context] ..., [:io.pedestal.http.tracing/otel-context] ..., [:io.pedestal.http.tracing/span] ...}}, :line 128}
DEBUG io.pedestal.interceptor.chain.debug - {:interceptor :io.pedestal.service.interceptors/log-request, :stage :enter, :execution-id 1, :context-changes nil, :line 128}
DEBUG io.pedestal.interceptor.chain.debug - {:interceptor :io.pedestal.http.route/query-params, :stage :enter, :execution-id 1, :context-changes nil, :line 128}
DEBUG io.pedestal.interceptor.chain.debug - {:interceptor :io.pedestal.http.body-params/body-params, :stage :enter, :execution-id 1, :context-changes nil, :line 128}
DEBUG io.pedestal.interceptor.chain.debug - {:interceptor :io.pedestal.http.csrf/anti-forgery, :stage :enter, :execution-id 1, :context-changes {:added {[:request :io.pedestal.http.csrf/anti-forgery-token] "Qgj7HzIwNM1EjVlsZzPi/ud/P7WAXh8mFPWV3ZjDz5ZMQXaxeg1PR1leom2k12jxvtcxZFI86fO3R4zA"}}, :line 128}
DEBUG io.pedestal.interceptor.chain.debug - {:interceptor :io.pedestal.http.route/query-params, :stage :enter, :execution-id 1, :context-changes nil, :line 128}
DEBUG io.pedestal.interceptor.chain.debug - {:interceptor :io.pedestal.http.ring-middlewares/resource, :stage :enter, :execution-id 1, :context-changes {:added {[:response] {:status 200, :headers {"Content-Length" "167", "Last-Modified" "Thu, 27 Mar 2025 23:04:20 GMT"}, :body ...}}}, :line 128}
DEBUG io.pedestal.interceptor.chain.debug - {:interceptor :io.pedestal.http.route/router, :stage :enter, :execution-id 1, :context-changes {:added {[:route] ...}}, :line 128}
DEBUG io.pedestal.interceptor.chain.debug - {:interceptor :io.pedestal.http.route/path-params-decoder, :stage :enter, :execution-id 1, :context-changes {:added {[:io.pedestal.http.route/path-params-decoded?] true}}, :line 128}
DEBUG io.pedestal.interceptor.chain.debug - {:interceptor :io.pedestal.http.secure-headers/secure-headers, :stage :leave, :execution-id 1, :context-changes {:added {[:response :headers "X-Frame-Options"] "DENY", [:response :headers "X-XSS-Protection"] "1; mode=block", [:response :headers "X-Download-Options"] "noopen", [:response :headers "Strict-Transport-Security"] "max-age=31536000; includeSubdomains", [:response :headers "X-Permitted-Cross-Domain-Policies"] "none", [:response :headers "X-Content-Type-Options"] "nosniff", [:response :headers "Content-Security-Policy"] "object-src 'none'; script-src 'unsafe-inline' 'unsafe-eval' 'strict-dynamic' https: http:;"}}, :line 128}
DEBUG io.pedestal.interceptor.chain.debug - {:interceptor :io.pedestal.http.csrf/anti-forgery, :stage :leave, :execution-id 1, :context-changes {:added {[:response :session] {"__anti-forgery-token" "Qgj7HzIwNM1EjVlsZzPi/ud/P7WAXh8mFPWV3ZjDz5ZMQXaxeg1PR1leom2k12jxvtcxZFI86fO3R4zA"}}}, :line 128}
DEBUG io.pedestal.interceptor.chain.debug - {:interceptor :io.pedestal.http.ring-middlewares/content-type-interceptor, :stage :leave, :execution-id 1, :context-changes {:added {[:response :headers "Content-Type"] "text/html"}}, :line 128}
DEBUG io.pedestal.interceptor.chain.debug - {:interceptor :io.pedestal.service.interceptors/not-found, :stage :leave, :execution-id 1, :context-changes nil, :line 128}
DEBUG io.pedestal.interceptor.chain.debug - {:interceptor :io.pedestal.http.tracing/tracing, :stage :leave, :execution-id 1, :context-changes {:changed {[:bindings] ...}, :removed {[:io.pedestal.http.tracing/otel-context-cleanup] ..., [:io.pedestal.http.tracing/prior-otel-context] ..., [:io.pedestal.http.tracing/otel-context] ..., [:io.pedestal.http.tracing/span] ...}}, :line 128}
DEBUG io.pedestal.interceptor.chain.debug - {:interceptor :io.pedestal.http.http-kit/response-converter, :stage :leave, :execution-id 1, :context-changes nil, :line 128}
DEBUG io.pedestal.interceptor.chain.debug - {:interceptor :io.pedestal.http.http-kit/async-responder, :stage :leave, :execution-id 1, :context-changes nil, :line 128}

You can also use curl or http to make a request:

$ http --json post :8080/hello name="Pedestal User"
HTTP/1.1 200 OK
Content-Security-Policy: object-src 'none'; script-src 'unsafe-inline' 'unsafe-eval' 'strict-dynamic' https: http:;
Content-Type: text/plain
Date: Thu, 27 Mar 2025 23:06:12 GMT
Server: Pedestal/http-kit
Strict-Transport-Security: max-age=31536000; includeSubdomains
X-Content-Type-Options: nosniff
X-Download-Options: noopen
X-Frame-Options: DENY
X-Permitted-Cross-Domain-Policies: none
X-Xss-Protection: 1; mode=block
content-length: 21

Hello, Pedestal User.


$

Starting the service

Alternately, you can start the service directly without starting a REPL:

$ clj -X:run
INFO  com.blueant.peripheral.main - {:msg "Service com.blueant/peripheral startup", :port 8080, :line 10}

At this point, the service is running; you can use another window to execute HTTP requests. If you open a browser window to http://localhost:8080/index.html, you’ll see the following logged to the service’s console:

INFO  io.pedestal.service.interceptors - {:msg "GET /index.html", :line 40}
INFO  io.pedestal.service.interceptors - {:msg "GET /favicon.ico", :line 40}

Gathering Telemetry

The template includes very basic support for gathering and reporting telementry using {otel}. For local work, this is best accomplished by running a Docker container with the Jaeger server running; the container will collect telemetry from the running application, and also provides a user interface to examine the traces produced by the application.

The template includes a script, start-jaeger.sh that downloads the necessary files and starts the container, and opens your web browser to the Jaeger UI:

$ ./start-jaeger.sh
Downloading Open Telemetry Java Agent to target directory ...
f7296a450ab2bfad684451ed7e0ed22125c0743f79e9675c4e15f593570986de
Jaeger is running, execute `docker stop jaeger` to stop it.
>

Stop your old REPL session, if necessary, and start a new one:

$ clj -A:test:otel-agent
OpenJDK 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended
[otel.javaagent 2025-03-27 16:12:48:691 -0700] [main] INFO io.opentelemetry.javaagent.tooling.VersionLogger - opentelemetry-javaagent - version: 2.14.0
Clojure 1.12.0
user=> (use 'dev)
nil
user=> (go!)
Routing table:
┏━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Method ┃  Path  ┃                 Name                 ┃
┣━━━━━━━━╋━━━━━━━━╋━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
┃   :get ┃ /hello ┃ :com.blueant.peripheral.routes/hello ┃
┃  :post ┃ /hello ┃ :com.blueant.peripheral.routes/greet ┃
┗━━━━━━━━┻━━━━━━━━┻━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
#object[io.pedestal.http.http_kit$create_connector$reify__17973 0x16a475d3 "io.pedestal.http.http_kit$create_connector$reify__17973@16a475d3"]
user=>```

The :otel-agent alias enables the Open Telementry Java Agent; a Java Agent is a special library that "hooks into"
the Java Virtual Machine, and can instrument classes as they are loaded from disk, or from JAR files.  In this
case, the agent will add code that initializes open telemetry in our application, and instrument the Jetty classes
to capture the real times when requests arrive and responses are sent.

In a separate window, you can open http://localhost:8080/hello or http://localhost:8080/index.html.  Your application
will handle the requests while gathering and sending tracing data to the Jaeger server running inside the Docker container.

After that, go back to the Jaeger UI, and select `com.blueant/peripheral` in the Service drop-down list footnote:[If `com.blueant/peripheral` isn't present,
you will need to refresh the browser so that it can populate the list of services.], then click "Find Traces".

image::jaeger-ui-search.png[]

You can then select a specific trace to get more details about it:

image::jaeger-ui-trace.png[]

[NOTE]
====
You don't _need_ to run your application with the Java agent in order to gather and send traces; however, the alternative
involves quite a bit more setup, and many additional dependencies for all the necessary Open Telemetry libraries.
====

## Other build commands

The `lint` command uses link:https://github.com/clj-kondo/clj-kondo[clj-kondo] to identify problems in your source code:

$ clj -T:build lint WARNING: update-vals already refers to: #'clojure.core/update-vals in namespace: clj-kondo.impl.analysis.java, being replaced by: #'clj-kondo.impl.utils/update-vals linting took 137ms, errors: 0, warnings: 0 clj-kondo approves ☺️

The `lint` command will exit with a -1 status code if there are linter errors; this aligns well with
using it inside a CI/CD pipeline.

The `jar` command builds a Maven POM file, and a JAR for the project:

$ clj -T:build jar Writing pom.xml…​ Copying source…​

Building JAR target/com.blueant/peripheral-0.1.0-SNAPSHOT.jar …​

There's also an `install` command to install the JAR to your local Maven repository, and a `deploy`
command, to deploy the JAR to link:https://clojars.org/[Clojars].


## Conclusion

The template provides a tiny amount of structure and examples; it's a seed
from which you can grow a full project, but small as it is, it's worth
exploring in more detail.

Can you improve this documentation? These fine people already did:
Howard M. Lewis Ship & Howard Lewis Ship
Edit on GitHub

cljdoc is a website building & hosting documentation for Clojure/Script libraries

× close