This is part three of a series teaching Clojure to people with little or no programming experience.
In Part 1 you ran a web server on your computer.
In Part 2 you put it on the internet.
In this part you'll go from a website that says Hello, World! to a website with a home page, an about page, and a polite message for URLs that don't exist.
Step 1: Look at what the server receives
Before we start building the new pages, let's look at the information available to handler.
Temporarily change handler in src/app/core.clj so it sends the request back to you as text:
(defn handler [req]
{:status 200
:headers {"Content-Type" "text/plain"}
:body (pr-str req)})
Two changes: _req is now req because handler is using it, and the body is now (pr-str req), which means "print this value as text".
Remember the rhythm from Part 1: edit, stop, start, refresh.
Stop the server if it's still running (Ctrl+C), start it again with clojure -M:run, and open http://localhost:8080/hello?name=carvalho.
You'll see something like this (shortened here):
{:remote-addr "0:0:0:0:0:0:0:1",
:headers {"accept" "text/html,...", "host" "localhost:8080", "user-agent" "Mozilla/5.0 ..."},
:server-port 8080,
:uri "/hello",
:query-string "name=carvalho",
:request-method :get,
...}
That's the request. It's a map, just like the response.
Two of its keys matter for this series:
-
:uriis the part of the address after the host name:/hello. We'll use it in this part to choose which page to show. -
:query-stringis whatever comes after the?:name=carvalho. We'll use it in the next part.
Change the address to /about or /anything-you-like and watch :uri change.
Now that you know what a request contains, let's build the new pages.
Step 2: Send back HTML instead of plain text
Put handler back to saying hello (and req back to _req, since we're not looking at the request for now), but make it send HTML text.
Change Content-Type from text/plain to text/html and write HTML in the body:
(defn handler [_req]
{:status 200
:headers {"Content-Type" "text/html"}
:body "<h1>Hello, World!</h1>"})
Content-Type tells the browser how to read what it receives.
text/plain means "show it as text", text/html means "this is an HTML page, interpret the tags".
<h1>...</h1> is the HTML tag for a big heading.
Restart, refresh, and the greeting becomes a heading.
If you're new to HTML, a few tags will get you far:
-
<h1>Title</h1>for a heading. -
<p>Some text</p>for a paragraph. -
<a href="https://example.com">a link</a>for a link. -
<ul>and<li>for a bulleted list: the<ul>is the list, and each<li>is one item in it. -
<nav>for a group of navigation links. You'll see it in the next step.
Step 3: Create a reusable page template
Every page of a website needs the same frame: a title, some styling, a navigation bar, and so on.
Typing that for each page would get old quickly.
So let's write a function that takes the title and the content of a page and wraps them in a frame.
Add this above handler (Clojure reads a file from top to bottom, so a function has to be defined before the code that uses it):
(defn page [title body]
(str "<!DOCTYPE html><html><head><title>" title "</title>"
"<style>body{font-family:sans-serif;max-width:600px;margin:40px auto;padding:0 20px}"
"nav a{margin-right:12px}</style></head><body>"
"<nav><a href=\"/\">Home</a><a href=\"/about\">About</a></nav>"
body
"</body></html>"))
page receives two things, title and body, the same way handler receives req.
Most of the text inside it is the frame itself. The <style> and <nav> lines are decoration, so you don't need to understand them yet. Just type them in.
Two things to notice:
-
strjoins things together into one piece of text.titleandbodyget dropped between the other pieces. - To put a double quote inside a piece of text you write
\", which is whyhref=\"/\"looks so busy. The backslash means "literally print this next character, I'm not trying to end the text."
Now use page to write two pages. Each one is a function with no inputs (that's the empty []), that returns HTML text:
(defn home-page []
(page "Home"
(str "<h1>Hi, I'm Carvalho</h1>"
"<p>Welcome to my corner of the internet.</p>"
"<ul>"
"<li><a href=\"https://github.com\">GitHub</a></li>"
"</ul>")))
(defn about-page []
(page "About"
(str "<h1>About Me</h1>"
"<p>I'm learning Clojure and building my first website!</p>")))
str shows up again here, joining the lines of each body into one piece of text before page wraps it.
Put your real name and real links in home-page.
Nothing changes in the browser yet. In the next step we'll connect these pages to addresses so you can see them.
Step 4: Show the right page
You have two pages and one handler. The handler needs to look at :uri, which you saw in Step 1, and choose what page to show.
Replace handler with:
(defn handler [req]
(case (:uri req)
"/" {:status 200
:headers {"Content-Type" "text/html"}
:body (home-page)}
"/about" {:status 200
:headers {"Content-Type" "text/html"}
:body (about-page)}))
(:uri req) looks up the key :uri in the request map. (It's a Clojure shortcut: a keyword in the first position looks itself up in the map that follows.)
case then picks the option that matches that value: "/" gets the home page and "/about" gets the about page.
Restart and visit / and /about. You have a two-page website, with a navigation bar that works.
It works, but look at the two response maps: they're identical except for the :body.
Add a third page and you'd copy it a third time, and a fourth, and so on.
Whenever you catch yourself repeating the same work, consider moving it to a function so it can be reused.
Here's a small helper function that builds the response map.
The only thing that changes between pages, the body, becomes its input.
Add it above handler:
(defn html-response [status body]
{:status status
:headers {"Content-Type" "text/html"}
:body body})
Now handler can shrink to this:
(defn handler [req]
(case (:uri req)
"/" (html-response 200 (home-page))
"/about" (html-response 200 (about-page))))
Restart and visit both pages again: nothing changed for the visitor, and your code is less repetitive.
The status is a parameter too, which is going to be handy in the next step.
Checkpoint: your website now has more than one page.
Step 5: Handle pages that don't exist
Visit http://localhost:8080/nothing-here.
Instead of something friendly, the browser shows No matching clause: /nothing-here.
case needs a match, and there wasn't one, so the server gave up (and answered with status 500, the web's code for "something broke on the server").
The fix is a catch-all as the last option: a page for "I don't know that one", with status 404, the web's code for "not found".
Add not-found-page above handler, and replace handler with the new version:
(defn not-found-page []
(page "Not Found" "<h1>404 - Page Not Found</h1>"))
(defn handler [req]
(case (:uri req)
"/" (html-response 200 (home-page))
"/about" (html-response 200 (about-page))
(html-response 404 (not-found-page))))
Look at the last line of case: it has only a result, with no match value in front of it.
case treats that as the default, the answer when nothing else matched.
Restart and try /nothing-here again.
The 404 page looks like any other page, so how do you know the status really is 404? Your browser keeps track.
Right-click the page, choose Inspect, open the Network tab and reload. The Status column shows 404 for /nothing-here, and 200 for / and /about.
Status codes are mostly for machines, like browsers and search engines, which need to know what happened without reading the page.
Checkpoint: your website now answers every address: with a page when it knows one, and with a friendly 404 when it doesn't.
The whole file
Here is the full src/app/core.clj at this point. If something isn't working, compare yours to this one:
(ns app.core
(:require [org.httpkit.server :as http])
(:gen-class))
(defn page [title body]
(str "<!DOCTYPE html><html><head><title>" title "</title>"
"<style>body{font-family:sans-serif;max-width:600px;margin:40px auto;padding:0 20px}"
"nav a{margin-right:12px}</style></head><body>"
"<nav><a href=\"/\">Home</a><a href=\"/about\">About</a></nav>"
body
"</body></html>"))
(defn home-page []
(page "Home"
(str "<h1>Hi, I'm Carvalho</h1>"
"<p>Welcome to my corner of the internet.</p>"
"<ul>"
"<li><a href=\"https://github.com\">GitHub</a></li>"
"</ul>")))
(defn about-page []
(page "About"
(str "<h1>About Me</h1>"
"<p>I'm learning Clojure and building my first website!</p>")))
(defn not-found-page []
(page "Not Found" "<h1>404 - Page Not Found</h1>"))
(defn html-response [status body]
{:status status
:headers {"Content-Type" "text/html"}
:body body})
(defn handler [req]
(case (:uri req)
"/" (html-response 200 (home-page))
"/about" (html-response 200 (about-page))
(html-response 404 (not-found-page))))
(defn -main [& _args]
(let [port (Integer/parseInt (or (System/getenv "PORT") "8080"))]
(println (str "Server running on port " port))
(http/run-server handler {:port port})))
Deploy it
Same command as in Part 2:
vercel deploy --prod
Vercel remembers your project from last time, so it shouldn't ask you the setup questions again.
Open the link and click around. Visit /about, and then /nothing-here to see your 404 page live.
You didn't need to change Dockerfile.vercel from Part 2: it copies the whole src folder, so your new code goes along automatically.
Your website is getting more interesting!
If something went wrong
-
The browser shows old content: the running program still has the old code. Stop the server with
Ctrl+C, start it again, and refresh. -
Unable to resolve symbol: html-response(orhome-page,page...): Clojure reads the file from top to bottom, so a function has to be defined above the code that uses it. Move it abovehandler, and check the spelling of the name. -
Syntax error reading sourceorUnmatched delimiter: a parenthesis is missing or extra. Remember the "Break it on purpose" section in Part 1: the line number in the error is where to start looking. -
The page shows
<h1>and other tags as text: theContent-Typeis stilltext/plain. It has to betext/html. -
No matching clause: /something:casedidn't find a match and has no default. Add the last line from Step 5.
What you learned
- The request is a map too:
:uritellshandlerwhich page the visitor asked for, and:query-stringholds what comes after the?. -
Content-Typetells the browser how to read the response.text/htmlmakes it interpret the HTML tags. -
strjoins pieces of text, andpageuses it to wrap any content in the same frame (title, styling, and navigation), so every page of your website looks similar. -
home-page,about-pageandnot-found-pageare functions that return HTML text, andhandlerusescaseto pick one based on:uri. - When you catch yourself repeating the same work, move it into a function.
html-responsedid that for the response map. - A function has to be defined above the code that uses it.
- Status codes are part of the response:
200means OK and404means not found. Without a default,casefails and the server answers with a500.
Next
In the next part we'll make a page that responds to what a user types, and you'll finally put :query-string from Step 1 to work.
Top comments (0)