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:
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):
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:
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):
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:
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:
(: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:
Now handler can shrink to this:
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:
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:
Deploy it
Same command as in Part 2:
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.