tcds.dev

Learning Clojure from Scratch (Part 1) - Hello, World

October 12, 2026

Part 1 of 5 ยท Learning Clojure from Scratch

This is part one of a series of blog posts that tries to teaches you Clojure from scratch. It assumes that you have little or no experience with programming.

By the end of the series you'll have your own Clojure website deployed to production: a homepage, an interactive page, and a blog.

Some of it will be hard, and you won't understand everything we do to get there. Try to trust the process. My primary goal is to help you experience success early (in the form of having your own website) in the hope that it motivates you to continue your learning journey. If you are curious and persistent, you should be able to complete this. Good luck!

In this first part we'll build a very small program and run it.

What you need

To check that Clojure is installed, open a terminal and run:

sh
clojure --version

If it prints a version number, you're ready.

Clojure runs on the JVM (Java Virtual Machine), and the installation guide walks you through getting that too, so there is nothing extra to download if the command above works. This series is written with macOS and Linux terminals in mind, but Windows should work too.

Create a project folder

Create an empty folder on your computer and call it my-site. Open Visual Studio Code, click File > Open Folder, and choose my-site.

You'll see an empty file explorer on the left. Everything you make in this series will live here.

Now click Terminal > New Terminal. A panel opens at the bottom of the window. A terminal is a place where you give your computer instructions by typing them. This one starts "inside" your my-site folder, which is exactly where you want it.

Optional: open the Extensions panel (the squares icon on the left), search for Calva, and install it. It will make it easier to work with Clojure files and will allow you to do REPL-driven development in the future.

Step 1: Tell Clojure about your project

Create a new file called deps.edn in the root of your project (hover over the file explorer and click the "New File" icon). Paste or type this in:

clojure
{:paths ["src"]
 :deps {org.clojure/clojure {:mvn/version "1.12.0"}
        http-kit/http-kit {:mvn/version "2.8.0"}}
 :aliases
 {:run {:main-opts ["-m" "app.core"]}}}

You don't have to understand all of this yet, but here is a quick tour:

  • :paths ["src"] says "my code lives in a folder called src".
  • :deps lists the libraries this project uses. Clojure itself is one of them. The other is http-kit, a library that speaks HTTP, the language of web browsers.
  • :aliases defines shortcuts. :run is a shortcut that means "start the program whose entry point is called app.core".

The whole file is a map: a collection of keys (the keywords starting with :) and the values that go with them.

Make sure to save the file before moving to the next step.

Step 2: Write the program

Create a new file at src/app/core.clj. That's a folder called src (src is short for source), with a folder called app inside it, with a file called core.clj inside that. (If you type src/app/core.clj as the file name, VS Code will create the folders for you.)

The name app.core in deps.edn matches this path: src/app/core.clj. Clojure uses the folders and file name to find your code.

One thing to know before we start: Clojure uses lots of parentheses. Almost everything is written as (something and-then-its-arguments): the first word says what to do, and the rest is what to do it with. For example, (println "Hello") means "print the text Hello". Every ( needs a matching ), and the same goes for [ with ] and { with }. VS Code highlights matching pairs as you type, and if you lose one, Clojure will tell you. We'll try that on purpose near the end.

We'll write the file in three small pieces.

Piece 1: Define a namespace and require some code

clojure
(ns app.core
  (:require [org.httpkit.server :as http]))

ns is short for namespace. The first line of your Clojure file defines the namespace you are working on (app.core) and the code it needs from other places in order to work. Here we require org.httpkit.server from http-kit and give it the nickname http so we don't have to type the long name every time.

Piece 2: Handle requests

Add this at the bottom:

clojure
(defn handler [_req]
  {:status  200
   :headers {"Content-Type" "text/plain"}
   :body    "Hello, World!"})

A website is like a conversation: a browser sends a request and the server sends back a response.

   browser                        your program
  ---------                    ------------------
  GET /  ---- request ------>  handler
         <--- response ------  {:status 200 ...}

handler is the function that handles what to send back. A function is a named piece of code that takes something in, does some work, and gives something back. Here, handler takes in the request and gives back the response.

  • defn defines a function. Here you named it handler.
  • [_req] is the thing the function receives: the request from the browser. The leading underscore is a convention that means "I'm not using this yet".
  • The {...} is a map that defines the response: :status is a code (200 means "OK"), :headers describes the format of the response, and :body is what shows up on the screen.

Notice that in Clojure, calling a function returns whatever its last expression evaluates to. handler return the map with :status, :headers and :body.

Piece 3: Create a main function and start the server

Add this at the bottom:

clojure
(defn -main [& _args]
  (println "Server running on port 8080")
  (http/run-server handler {:port 8080}))

-main is the function that runs when you start your program. It prints a message, then starts a server with your handler.

& _args is for words typed after the program's name on the terminal. We're not using any, so ignore it for now.

8080 is a port, which you can think of as a numbered door on your computer. Browsers will knock on door 8080 to talk to your program.

Here is the complete file:

clojure
(ns app.core
  (:require [org.httpkit.server :as http]))

(defn handler [_req]
  {:status  200
   :headers {"Content-Type" "text/plain"}
   :body    "Hello, World!"})

(defn -main [& _args]
  (println "Server running on port 8080")
  (http/run-server handler {:port 8080}))

Save it. Your project should now look like this:

my-site/
  deps.edn
  src/
    app/
      core.clj

Run it

In the terminal at the bottom of VS Code, type this and hit enter:

sh
clojure -M:run

The first time, Clojure downloads the libraries from deps.edn. It can take a minute. When it's done you'll see the message:

Server running on port 8080

The terminal now looks stuck, with no prompt coming back. That's correct: your server is running and waiting for visitors.

Open http://localhost:8080 in your browser. localhost is a name that basically means "this computer". You should see:

Hello, World!

Checkpoint: you just wrote a program that serves a web page. It works only on your computer, but you created a real (and very simple) web page!

Change something

Go back to src/app/core.clj and change the body from "Hello, World!" to "Hello, Clojure!". Save the file and refresh your browser.

Nothing changed. That's expected: the running program loaded the old code and doesn't know that the file changed. Go to the terminal, press Ctrl+C to stop the server, then run clojure -M:run again and refresh the browser.

This is the rhythm for the next few parts: edit, stop, start, refresh. (Clojure has a smarter way of working called REPL-driven development. We'll start using it in a future post.)

Break it on purpose

Reading errors is a good portion of programming, and it's easier when you know what broke. To get some practice, delete the very last character of the :body line (the ) right after the }), save, stop the server, and start it again.

You'll get an error that starts with Syntax error reading source and says EOF while reading, starting at line 4. It's Clojure telling you "I reached the end of the file (EOF) and something that opened on line 4 was never closed". Line 4 is where handler begins, so that's the place to look. Put the ) back, restart, and you're back in business.

When you see a wall of error text, don't panic. The useful part is usually in the first or second line.

If something went wrong

  • clojure: command not found: Clojure isn't installed yet, or the terminal was opened before the install finished. Close VS Code, reopen it, and try again.
  • Address already in use: another program, or an earlier run of this one, is using port 8080. You'll still see Server running on port 8080 just before the error, because the message prints before the server tries to claim the port. Find the terminal where it's still running and press Ctrl+C. Alternatively, change your code to use another port (say 8081).
  • An error mentioning Could not locate app/core: the file isn't at src/app/core.clj. Check the folder and file names.
  • The browser says it can't connect: the server isn't running. Look at the terminal for an error, or for the prompt coming back, which means the program exited.

What you learned

  • Your website works like a conversation: a browser sends a request, and your program sends back a response.
  • A function takes something in, does some work, and returns something. handler, for example, takes the request and gives back a map describing the response.
  • deps.edn says what the project needs and creates aliases (like clojure -M:run).
  • src/app/core.clj holds the code for your handler (what to say) and -main (the starting point).
  • An error message is Clojure telling you where it got lost. Start reading the first and second lines before you panic.

Next

In a future blog post we'll make handler smarter and we'll make a few small changes so the same program can run on someone else's computer via the internet.

Bonus: now that you are technically a Clojure developer, why not join the Clojurians community on Slack?