Hello World, With Parameters

Welcome Back

Now that you’ve gone through the basics in the Hello World guide it’s time to go one level deeper. We’re going to add two features. That will be our vehicle to talk about some important concepts in Pedestal.

What You Will Learn

After reading this guide, you will be able to:

  • Accept query parameters.

  • Use the request map.

  • Apply logic in your handler.

  • Conditionally return an error.

  • Return HTML in your response.

Guide Assumptions

Like Hello World, this guide is for beginners who are new to Pedestal and may be new to Clojure. It doesn’t assume any prior experience with a Clojure-based web framework. You should be familiar with the basics of HTTP: URLs, response codes, and content types.

If you’ve already done some of those other things, you might want to skip ahead to Your First API to start building some logic and multiple routes.

This guide also assumes that you are in a Unix-like development environment, with Java installed. We’ve tested it on Mac OS X and Linux (any flavor) with great results. We haven’t yet tried it on the Windows Subsystem for Linux, but would love to hear from you if you’ve succeeded with it there.

Getting Help If You’re Stuck

We’ll take this in small steps. If you get stuck at any point in this guide, please submit an issue about this guide or, hop over to the Pedestal Users mailing list and raise your hand there. You can also get help from the #pedestal channel on the Clojurians Slack.

Where We Are Going

In this guide, we will build on the hello.clj example from the previous guide. We will enhance it to take a single parameter in the URL and return a friendly greeting to that person.

There are some people we don’t want to meet, though. So we’ll also include a list of names that should not be said. Then we can make our handler function a bit smarter. If any forbidden name is uttered, our handler will return a 404 response instead of a cordial greeting.

Before We Begin

If you worked through Hello World, then you already have all the files you need. If not, take a moment to grab the sources from the repository. That will be our starting point for enhancements this time.

Accepting Parameters

HTTP offers a few different ways for the client to send data up to our service. The most basic is probably the URL query parameter. We usually see these on GET requests, but nothing stops a client from using them on other request types.

Example 1. URL with Query Parameter

Because query parameters are so common, Pedestal handles them automatically. You can try that URL with your service right now.

As a reminder, we’re running the server by using the clj tool to start an interactive session (called a REPL, rhymes with ripple) then starting the service in that session. Before we do that, though, let’s discuss interactive development.

Interactive Development

It’s really not optimal to be restarting your REPL in order to restart the service every time you need to make a change. In real Clojure development, we rarely restart the REPL. Instead, we make our system friendly to interactive development.

src/hello.clj
(defn create-connector []
  (-> (conn/default-connector-map 8890)
      (conn/with-default-interceptors)
      (conn/with-routes routes)
      (hk/create-connector nil)))

;; For interactive development
(defonce *connector (atom nil))                             (1)

(defn start []
  (reset! *connector                                        (2)
          (conn/start! (create-connector))))

(defn stop []
  (conn/stop! @*connector)
  (reset! *connector nil))

(defn restart []                                            (3)
  (stop)
  (start))
1 defonce here means we can recompile this file in the same REPL without overwriting the *connector symbol’s value.
2 reset! replaces the current value in the atom with the new value.
3 This is a quick way to bounce the server after reloading code.

First, let’s start the Pedestal connector.

$ clj
Clojure 1.12.0
user=> (require 'hello)
nil
user=> (hello/start)
#object[io.pedestal.http.http_kit$create_connector$reify__15862 0x3b85a820 "io.pedestal.http.http_kit$create_connector$reify__15862@3b85a820"]
user=>

The start function has done four things:

  • It has created the connector (via create-connector)

  • It has started the connector

  • It has stored the connector in an atom (a mutable container).

  • It has returned the connector, which the Clojure REPL has printed.

A convention for mutable objects, such as atoms, is to prefix their name with a *. This is a reminder that we must use the special prefix @ to dereference the actual value (the connector object) from the container (the atom).

Now we can use start, stop, and restart functions as we make changes. Mostly, we’ll make code changes, reload the hello namespace, and call restart to quickly stop the connector and start a new one.

Moving along

From another window, let’s send a request with a query parameter.

$ curl -i http://localhost:8890/greet\?name=Michael (1)
HTTP/1.1 200 OK
Strict-Transport-Security: max-age=31536000; includeSubdomains
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
X-Xss-Protection: 1; mode=block
X-Download-Options: noopen
X-Permitted-Cross-Domain-Policies: none
Content-Security-Policy: object-src 'none'; script-src 'unsafe-inline' 'unsafe-eval' 'strict-dynamic' https: http:;
Content-Type: text/plain
content-length: 14
Server: Pedestal/http-kit
Date: Fri, 11 Apr 2025 23:37:30 GMT

Hello, world!
1 The backslash before name is needed because otherwise the shell will treat the ? specially.

What we’ve demonstrated is that Pedestal can route the incoming request, GET /greet?name=Michael and isn’t confused by the presence of the query parameter. Ultimately, the greet-handler function was invoked, and did its job. To update greet-handler to match our goal, we first have to discuss the request argument passed to it.

Also, if you look in the console of your REPL, you’ll see that Pedestal logged the request:

[] INFO io.pedestal.service.interceptors - {:msg "GET /greet", :line 40}

You’ll may see slightly different results, but the takeaway is that you can look in the REPL session to verify that requests are being processed by your server.

The Request Map

Here’s a reminder of what greet-handler currently looks like:

(defn greet-handler [_request]                              (1)
  {:status 200
   :body   "Hello, world!\n"})                              (2)
1 The function takes one argument, which we call _request
2 …​ and it returns a map.

We aren’t using the _request argument right now, so we don’t really know what information it provides.

Let’s take a look at that request by echoing it back to the client. This is a common debugging trick that you can use.

(defn greet-handler [request]                               (1)
  {:status 200 :body request})                              (2)
1 We’re actually using the parameter now, so no more leading _ on the parameter name.
2 Instead of returning the string "Hello, world!" we return the entire request as the response body.

Make sure you’ve update the hello.clj file, then restart your service with that new definition:

user=> (require :reload 'hello)
nil
user=> (hello/restart)
#object[io.pedestal.http.http_kit$create_connector$reify__15862 0x6f4fa62f "io.pedestal.http.http_kit$create_connector$reify__15862@6f4fa62f"]
user=>

Again, this is in two steps: (require :reload …​) to update all the Clojure code, then (hello/restart) to stop the connector and start a replacement, using the update functions.

With the updated code live, you can use curl again to see the change in the service’s behavior:

$ curl -i http://localhost:8890/greet\?name=Michael
HTTP/1.1 200 OK
Strict-Transport-Security: max-age=31536000; includeSubdomains
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
X-Xss-Protection: 1; mode=block
X-Download-Options: noopen
X-Permitted-Cross-Domain-Policies: none
Content-Security-Policy: object-src 'none'; script-src 'unsafe-inline' 'unsafe-eval' 'strict-dynamic' https: http:;
Content-Type: application/edn (1)
content-length: 763
Server: Pedestal/http-kit
Date: Fri, 11 Apr 2025 23:42:09 GMT

{:remote-addr "127.0.0.1", :start-time 360038830750564, :params {:name "Michael"}, :headers {"accept" "*/*", "host" "localhost:8890", "user-agent" "curl/8.7.1"}, :async-channel #object[org.httpkit.server.AsyncChannel 0x525383f "/127.0.0.1:8890<->/127.0.0.1:63835"], :server-port 8890, :content-length 0, :websocket? false, :query-params {:name "Michael"}, :content-type nil, :path-info "/greet", :character-encoding "utf8", :url-for #object[clojure.lang.Delay 0x4adfdcd0 {:status :pending, :val nil}], :uri "/greet", :server-name "localhost", :query-string "name=Michael", :path-params {}, :body nil, :scheme :http, :request-method :get}
1 Pedestal uses the application/edn content type when the response :body is a Clojure map. EDN is to Clojure as JSON is to JavaScript.

Yikes! There’s a lot of content in the request map echoed back to us, and it’s in one huge line.

We need to unpack that a bit to make sense out of it. Let’s add some line breaks and indentation to make it easier to read. We’ll also omit several keys. They may be relevant later, but they’re a distraction now.

Here’s what we are left with.

{:request-method     :get
 :uri                "/greet"
 :query-string       "name=Michael"
 :query-params       {:name "Michael"}
 :params             {:name "Michael"}
 :headers            {"accept"     "*/*"
                      "host"       "localhost:8890"
                      "user-agent" "curl/8.7.1"}
 :remote-addr        "127.0.0.1"
 :server-port        8890
 :content-length     0
 :content-type       nil
 :path-info          "/greet"
 :character-encoding "utf8"
 :server-name        "localhost"
 :path-params        {}
 :body               nil
 :scheme             :http}

This is a request map. Again, a generic Clojure map with specific keys. You access its values with the get and get-in functions. For now, we’ll look at three interesting keys.

The whole query string appears under the :query-string key. That’s the portion of the URL after, but not including, the ? character. The query string can be a pain to deal with. It is URL-encoded, which means that some common characters, such as spaces and punctuation, are converted into sequences of several characters. There’s also delimeters between name/value pairs in the query string.

Pedestal parses and slices all that up into a map of parameters for you, which is attached to the request map as the :query-params key. Each parameter name is converted into a keyword. Parameter values are always strings, though, so you may still have to parse them a bit.

Pedestal has several different sources of parameters in the request, which we’ll cover in more detail later. The :params key is the merged map of all params from all sources; :query-params is just the parameters from the query string. You get to choose which of these is right for your particular handler.

Using the Parameter

Inside our handler function, the :name parameter is available inside a nested map. Let’s dig it out and add it to our response.

src/hello.clj
(defn greet-handler [request]
  (let [green-name (get-in request [:query-params :name])]  (1)
    {:status 200 :body (str "Hello, " green-name "\n")}))   (2)
1 Extract the parameter, bind it to the name greet-name.
2 For the response body, concatenate "Hello," and the parameter into a string.

The let form is used to define one or more local symbols, much like a block-scoped local variable in a more traditional language.

Here we can just grab the value right out of the map and use it in the response.

Do the restart/reload dance again, and then use curl to try a few different requests with parameters.

$ curl http://localhost:8890/greet\?name=Michael
Hello, Michael
$ curl http://localhost:8890/greet\?name=Pankaj
Hello, Pankaj
$ curl http://localhost:8890/greet\?name=Geeta
Hello, Geeta
$ curl http://localhost:8890/greet\?name=ไอรีนซีพีชาไอที (1)
Hello, ไอรีนซีพีชาไอที
$ curl http://localhost:8890/greet\?name=No%20One%20To%20Be%20Trifled%20With (2)
Hello, No One To Be Trifled With
$ curl http://localhost:8890/greet\?name=
Hello,
$ curl http://localhost:8890/greet
Hello,
1 Unicode, amazingly, doesn’t need to be URL-escaped.
2 …​ but common characters such as space, need to be converted (to %20).

Dang. We were doing really well right up until the end there. Obviously we need to do something smarter when the input is missing. Bad user input is a way of life on the web, so let’s not make an error out of this. Instead, we just won’t personalize the greeting.

Sidequest: Why greet-name?

You might have noticed that the name query parameter in the URL shows up as the :name key in the :query-params map, but ultimately ends up in a local symbol, greet-name.

It is tempting to just use the symbol name here as well, for consistency, rather than the longer and less obvious greet-name.

Long experience has shown that some symbol names, those that conflict with functions and macros automatically imported from the clojure.core namespace, can cause you great frustration. Using name here shadows the function clojure.core/name.

Consider this easy mistake:

src/hello.clj
(defn greet-handler [request]
  (let [greet-name (get-in request [:query-params :name])]
    {:status 200 :body (str "Hello, " name "\n")}))

This looks correct at a glance, but consider what happens when it is executed:

$ curl -i  http://localhost:8890/greet\?name=Michael
HTTP/1.1 200 OK
Strict-Transport-Security: max-age=31536000; includeSubdomains
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
X-Xss-Protection: 1; mode=block
X-Download-Options: noopen
X-Permitted-Cross-Domain-Policies: none
Content-Security-Policy: object-src 'none'; script-src 'unsafe-inline' 'unsafe-eval' 'strict-dynamic' https: http:;
Content-Type: text/plain
content-length: 34
Server: Pedestal/http-kit
Date: Sat, 12 Apr 2025 00:16:42 GMT

Hello, clojure.core$name@6b202ecb

clojure.core$name@6b202ecb is how the clojure.core/name function is printed as a string. We can verify that in the REPL:

user=> (str clojure.core/name)
"clojure.core$name@6b202ecb"

So because we used greet-name in one place and name in the other we got really strange results; this is partly because Clojure, as a dynamically typed language, is very permissive …​ to a point:

This is Clojure …​ we don’t slap your wrists when you are wrong, we just punish you deeply for it later.
— Stuart Halloway

So, the moral of the story is to avoid shadowing names! With that, let us continue on to handling bad input in the request.

Handling Bad Input

In those last couple of curl calls, we did not properly pass up a name query parameter:

$ curl http://localhost:8890/greet\?name=
Hello,
$ curl http://localhost:8890/greet
Hello,

This is not desirable behavior; we should do better than returning half a sentence.

src/hello.clj
(defn greet-handler [request]
  (let [greet-name (get-in request [:query-params :name])
        message    (if (empty? greet-name)                  (1)
               "Hello, world!\n"                            (2)
               (str "Hello, " greet-name "\n"))]            (3)
    {:status 200 :body message}))                           (4)
1 Both nil and a zero-length string count as empty.
2 This is our fallback greeting message.
3 And our personalized greeting message.
4 Generating the response.

Again, reload and restart, then try new requests with curl:

$ curl http://localhost:8890/greet
Hello, world!
$ curl http://localhost:8890/greet\?name=
Hello, world!
$ curl http://localhost:8890/greet\?name=ไอรีนซีพีชาไอที
Hello, ไอรีนซีพีชาไอที

Much better! But our handler function is getting a bit unwieldy. I don’t mean that it is a whole six lines long, but rather that it mixes too many concerns. This function parses inputs, applies "domain" logic, and deals with creating a response body. It would be better to refactor this to separate those concerns.

src/hello.clj
(defn ok [message]                                          (1)
  {:status 200 :body message})

(defn greeting-for [greet-name]                             (2)
  (if (empty? greet-name)
    "Hello, world!\n"
    (str "Hello, " greet-name "\n")))

(defn greet-handler [request]                               (3)
  (let [greet-name (get-in request [:query-params :name])
        resp       (greeting-for greet-name)]
    (ok resp)))
1 This function can generate any "OK" result.
2 Our logic is now separated.
3 The handler now coordinates the rest.

This version separates the concerns nicely. One big benefit is that the greeting-for function is now easier to test. It’s just a pure function - no side effects - that doesn’t require any HTTP or Pedestal machinery to exercise. Likewise, that ok function is also side effect free and quite easy to test. Both functions are likely to get more complex, but each one will only deal with its own complexity.

In this example, we’re treating the case of an empty name the same as if the name parameter just isn’t included. See if you can make the handler more strict: it should return a "400 Bad Request" response if the name parameter is present but zero length.

Conditional Responses

There are some names that should not be spoken. Let’s enhance our service to avoid them. This will be a relatively simple change. We can hardcode the names as a set, then use that set as a function. (Clojure sets act as functions that test for the presence of their argument in the set itself.)

This part needs to go before greeting-for because we’ll use it to make a decision in that function.

src/hello.clj
(def unmentionables #{"YHWH"
                      "Voldemort"
                      "Mxyzptlk"
                      "Rumplestiltskin"
                      "曹操"})

Now we need to think about our greeting-for function. Right now it returns a string. We want to make sure the user gets a 404 if the query names one of the forbidden ones. We could have greeting-for directly return the response map, but that starts to entangle the concerns we just factored out. We’ll do the simplest thing that could possibly work: when greeting-for returns nil (instead of a string), that means the request failed.

src/hello.clj
(defn not-found []
  {:status 404 :body "Not found\n"})

(defn greeting-for [greet-name]
  (cond
    (unmentionables greet-name) nil
    (empty? greet-name) "Hello, world!\n"
    :else (str "Hello, " greet-name "\n")))

(defn greet-handler [request]
  (let [greet-name (get-in request [:query-params :name])
        message    (greeting-for greet-name)]
    (if message
      (ok message)
      (not-found))))

Because there are now three possible conditions that can happen in greet-handler, I’ve changed from using an if expression to a cond expression. cond uses pairs of "condition / value" clauses. So if (unmentionables greet-name) returns any truthy value, then cond returns nil and stops evaluating. If (empty? greet-name) returns true, then cond returns the literal string "Hello, world!\n" and nothing else. If neither of those happened, then cond sees the keyword :else as a condition[1] and the code continues to build the string the same way as before.

A nested if would also have worked here. In fact cond is a macro that expands into a series of nested if expressions. All three of these conditions are kind of parallel in significance, so I like that they appear to be parallel in the structure of the code. The shape of the code mirrors the shape of my thinking about these cases.

The second thing you’ll notice is that the handler now makes a decision whether to call our ok helper or the new function not-found. I do hope you weren’t expecting much in not-found! It’s as simple as it gets.

Restart your service and give it a try:

$ curl http://localhost:8890/greet\?name=Michael
Hello, Michael
$ curl http://localhost:8890/greet\?name=Rumplestiltskin
Not found
$ curl http://localhost:8890/greet\?name=曹操
Not found
$ curl http://localhost:8890/greet\?name=voldemort
Hello, voldemort
$ curl http://localhost:8890/greet\?name=He%20who%20must%20not%20be%20named
Hello, He who must not be named

Looks like it mostly works, though it has some trouble with different capitalization. Take a look at the docs for clojure.string and see if you can figure out how to make the comparison case-insensitive.

The Whole Shebang

Once again, we built this thing in small steps, so it may seem like there was a lot to deal with. The final product is still compact and to the point. Here’s all the final code.

src/hello.clj
(ns hello                                                   (1)
  (:require [io.pedestal.connector :as conn]                (2)
            [io.pedestal.http.http-kit :as hk]))            (3)

(def unmentionables #{"YHWH"
                      "Voldemort"
                      "Mxyzptlk"
                      "Rumplestiltskin"
                      "曹操"})

(defn not-found []
  {:status 404 :body "Not found\n"})

(defn greeting-for [greet-name]
  (cond
    (unmentionables greet-name) nil
    (empty? greet-name) "Hello, world!\n"
    :else (str "Hello, " greet-name "\n")))

(defn greet-handler [request]
  (let [greet-name (get-in request [:query-params :name])
        message    (greeting-for greet-name)]
    (if message
      (ok message)
      (not-found))))

(def routes
  #{["/greet" :get greet-handler :route-name :greet]}) 

(defn create-connector []
  (-> (conn/default-connector-map 8890)
      (conn/with-default-interceptors)
      (conn/with-routes routes)
      (hk/create-connector nil)))

;; For interactive development
(defonce *connector (atom nil))                             (1)

(defn start []
  (reset! *connector                                        (2)
          (conn/start! (create-connector))))

(defn stop []
  (conn/stop! @*connector)
  (reset! *connector nil))

(defn restart []                                            (3)
  (stop)
  (start))
deps.edn
{:paths ["src"]
 :deps  {io.pedestal/pedestal.http-kit {:mvn/version "0.8.0-alpha-2"}
         org.slf4j/slf4j-simple        {:mvn/version "2.0.17"}}}

The Path So Far

In this guide, we built upon Hello World to add:

  • Query parameters

  • General response functions

  • "Domain" logic

  • Conditional responses

We also learned about the request map, which is central to any non-trivial handler, as well as one approach to dealing with invalid user input in the request.

Where to Next?

So far, almost all our responses have been plain text.

Plain text is ugly, so we want to be able to return HTML. Rich clients don’t like HTML so much, so we also want to return JSON sometimes. The next stop on this trail looks at how Pedestal handles content types and response bodies. This will also be our first taste of interceptors, Pedestal’s core unit of work.


1. Using :else for this purpose is just a common Clojure convention, any non-false/non-nil value would work equally well.