(defn hello-routes [deps]
["/hello" (1)
(yada/handler "Hello World!\n") (2)
])
In this chapter we examine the "Hello World!" resource in depth. If you have followed the Getting Started chapter, you’ll be up and running and ready to try the examples yourself, but it’s up to you.
Let’s look at the route definition in the file src/edge/hello.clj
(the same one we saw in the Getting Started chapter). This function returns a route structure. The simplest route structure is a pair of elements contained in a vector.
(defn hello-routes [deps]
["/hello" (1)
(yada/handler "Hello World!\n") (2)
])
1 | The first element is a path (or pattern) which matches on the URI’s path when handling HTTP requests. |
2 | The second element is often a handler function (but can be another route structure, recursively). |
In this route structure, any incoming request with a path of /hello
gets sent to the yada handler defined by (yada/handler "Hello World!\n")
.
Let’s focus on this handler. We could have used a standard Ring function (fn [req] {…})
here but instead we created one with yada/handler
, which takes a resource and turns it into a handler function.
A resource is a Clojure map (or more accurately, a Clojure record) that completely describes the methods, properties, representations, security and other miscellaneous properties of a web resource, as data. The reason we can use a string ("Hello World"
) here is because yada contains logic to coerce a string into a resource.
There are a number of built-in coercions from various Clojure types and of course you can provide your own. |
Let’s send a request which gets routed to our handler, which creates the response. Let’s examine this response in more detail, via curl.
$ curl -i http://localhost:3000/hello
The -i option to curl shows us the HTTP status and response headers as well as the body, which is very useful for debugging. If you ever need to see the request headers too, add -v .
|
The response should be something like following (note that headers may appear in a different order):
HTTP/1.1 200 OK Server: Aleph/0.4.1 Connection: Keep-Alive Date: Fri, 17 Jun 2016 16:44:23 GMT Last-Modified: Fri, 17 Jun 2016 16:43:02 GMT ETag: fa863bd7ff53786d286e4bb3c0134416 Content-Type: text/plain;charset=utf-8 Vary: accept-charset Content-Length: 23 X-Frame-Options: SAMEORIGIN X-XSS-Protection: 1; mode=block X-Content-Type-Options: nosniff
The first three response headers are added by our webserver, Aleph.
Server: Aleph/0.4.0 Connection: Keep-Alive Date: Fri, 17 Jun 2016 16:44:23 GMT
Next we have another date and a string known as the entity tag:
Last-Modified: Sun, 09 Aug 2015 07:25:10 GMT ETag: fa863bd7ff53786d286e4bb3c0134416
The Last-Modified
header shows when the string Hello World!
was created, which happens to be the last time the system was started (or reset). Java strings are immutable, so yada is able to deduce that the
string’s creation date is also the last time it could have been modified.
The entity tag is computed from the value of the Hello World!
itself. Unlike the Last-Modified
value, it can survive a reset.
Both Last-Modified
and ETag
are used to support HTTP conditional requests and conflict detection when uploading new versions of a resource.
Next we have a header telling us the media-type of the string’s representation.
Content-Type: text/plain;charset=utf-8
yada is able to determine that the media-type is text, but without more information it must default to text/plain
. It can also tell us the charset, which defaults to the default charset of the JVM (almost always utf-8
)
Vary: accept-charset
Since the Java platform can encode a string to a number of charsets, yada adds a Vary header to signal to the user-agent (and proxy caches in between) that the body can change if a request contained a different Accept-Charset header. Java installations support many different charsets, so yada does too.
Next we are given the length of the body.
Content-Length: 13
This value is in bytes, regardless of the charset. It includes the newline.
Finally we see our response body.
Hello World!
In HTTP, a conditional request is one where a user-agent (like a browser) can ask a server for the state of the resource but only if a particular condition holds.
A common condition is whether the resource has been modified since a particular date, usually because the user-agent already has a copy of the resource’s state which it can use if possible. If the resource hasn’t been modified since this date, the server can tell the user-agent that there is no new version of the state.
We can test this by setting the If-Modified-Since header in the request.
Here’s how we might do this using the curl command.
$ curl -i http://localhost:3000/hello -H "If-Modified-Since: Mon, 1 Jan 2525 00:00:00 GMT"
Of course, nobody will have modified the resource since the year 2525, so we should get a 304 response, telling us we can use our cached copy:
HTTP/1.1 304 Not Modified X-Frame-Options: SAMEORIGIN X-XSS-Protection: 1; mode=block X-Content-Type-Options: nosniff Content-Length: 0 Vary: accept-charset ETag: fa863bd7ff53786d286e4bb3c0134416 Server: Aleph/0.4.1 Connection: Keep-Alive Date: Wed, 22 Jun 2016 16:57:46 GMT
Notice we also get the same Vary
and ETag
headers. These help any proxies between the user-agent and the service properly cache content, and if they would have been produced in a 200 response, then they must also be produced in a 304. (This is the kind of thing that yada takes care of for you, unlike most libraries).
The responses we have received back from our service all contain this curious header called Vary
set to accept-charset
. The server is telling us (and any proxies between us) that the representation might vary depending on the charset negotiated. Let’s see if we can get our "Hello World!" message returned in other charsets.
Let’s try getting the string in UTF16 by telling the server that’s the only charset we’ll accept:
curl -i http://localhost:3000/hello -H "Accept-Charset: UTF-16"
This returns the following:
HTTP/1.1 200 OK X-Frame-Options: SAMEORIGIN X-XSS-Protection: 1; mode=block X-Content-Type-Options: nosniff Content-Length: 28 Content-Type: text/plain;charset=utf-16 Last-Modified: Sun, 26 Jun 2016 11:11:31 GMT Vary: accept-charset ETag: 43b1f79e8efe0fa97c32901fbd5746d6 Server: Aleph/0.4.1 Connection: Keep-Alive Date: Mon, 27 Jun 2016 07:45:07 GMT ��Hello World!
The "Hello World!" message is prepended with 2 bytes called the Byte Order Mark (BOM). The length of the string (including the newline) is 13 characters. Since each character here is 2 bytes, that makes 26. The additional of the BOM makes it 28, which is what our Content-Length
header reports.
A BOM indicates the order that the 2 bytes are transmitted in. In 'big endian' form the most-significant byte is transmitted first. We can tell the service that we only want the big endian form with the following:
curl -i http://localhost:3000/hello -H "Accept-Charset: UTF-16BE"
This will now produce the message without the BOM, because it is unnecessary. This means our Content-Length
will be exactly 13 * 2 = 26.
HTTP/1.1 200 OK … Content-Length: 26 Hello World!
If we were to use UTF-32, which defaults to big-endian, we’ll get a Content-Length of 13 * 4 = 52.
HTTP/1.1 200 OK … Content-Length: 52 Hello World!
Note also that different representations generate different ETag
values. The entity tag is a way of managing a cache of representations, not a cache of resources. Think of the ETag
value as the key you could use in a key/value store that stored a cache of representations.
The negotiation of charsets may be considered by some to be unnecessary given the dominance of UTF-8. That is certainly true for today’s modern browsers. However, there are many other types of devices that are being connected to the internet (under the banner 'Internet of Things'). Many of these devices have very tight constraints on processing and memory which prevent them from supporting UTF-8. If we are building a web service, we may want to connect these devices to it in the future.
Of course it is not just charsets that can be negotiated. Another example is languages. Our "Hello World!" string is in English. Let’s provide support for simplified Chinese.
This calls for a different implementation:
(defn hello-language []
["/hello-language"
(yada/resource (1)
{:methods
{:get (2)
{:produces
{:media-type "text/plain"
:language #{"en" "zh-ch;q=0.9"}} (3)
:response
#(case (yada/language %) (4)
"zh-ch" "你好世界\n"
"en" "Hello World!\n")}}})])
1 | Using the yada/resource function to create a custom resource |
2 | The resource has a single method, GET |
3 | English is preferred, but Simplified Chinese is available too |
4 | This is a function that is given a context as the first argument. The yada/language convenience function pulls out the negotiated language from this context |
Let’s test this by providing a request header which indicates a preference for simplified Chinese:
$ curl -i http://localhost:3000/hello-language -H "Accept-Language: zh-CH"
We should get the following response:
HTTP/1.1 200 OK X-Frame-Options: SAMEORIGIN X-XSS-Protection: 1; mode=block X-Content-Type-Options: nosniff Content-Length: 13 Content-Type: text/plain Content-Language: zh-ch Vary: accept-language Server: Aleph/0.4.1 Connection: Keep-Alive Date: Mon, 27 Jun 2016 08:20:59 GMT 你好世界
There’s a lot more to content negotiation than this simple example can show. It is covered in depth in subsequent chapters.
Let’s try to overwrite the string by using a PUT
.
$ curl -i http://localhost:3000/hello -X PUT -d "Hello Wonderful World!%0a"
The response is as follows:
HTTP/1.1 405 Method Not Allowed Allow: GET, HEAD, OPTIONS Content-Length: 284 Content-Type: application/json Server: Aleph/0.4.1 Connection: Keep-Alive Date: Mon, 27 Jun 2016 08:56:58 GMT
The response status is 405 Method Not Allowed
, telling us that our request was unacceptable. There is also an Allow header, telling us which methods are allowed. One of these methods is OPTIONS, which we could have used to check whether PUT was available without actually attempting it.
$ curl -i http://localhost:3000/hello -X OPTIONS
The response should be:
HTTP/1.1 200 OK Allow: GET, HEAD, OPTIONS Content-Length: 0 X-Frame-Options: SAMEORIGIN X-XSS-Protection: 1; mode=block X-Content-Type-Options: nosniff Server: Aleph/0.4.1 Connection: Keep-Alive Date: Mon, 27 Jun 2016 09:00:27 GMT
Both the PUT
and the OPTIONS
response contain an Allow header which tells us that PUT
isn’t possible. This makes sense, because we can’t mutate a Java string.
We could, however, wrap the Java string in a Clojure atom which could reference different Java strings at different times.
To demonstrate this, try the following with the identifier http://localhost:3000/hello-atom
.
(yada/as-resource (atom "Hello World!\n"))
Let’s try a normal GET.
$ curl -i http://localhost:3000/hello-atom
We can now make another OPTIONS
request to see whether PUT
is available, before trying it.
$ curl -i http://localhost:3000/hello-atom -X OPTIONS
HTTP/1.1 200 OK Allow: GET, DELETE, HEAD, OPTIONS, PUT Content-Length: 0 X-Frame-Options: SAMEORIGIN X-XSS-Protection: 1; mode=block X-Content-Type-Options: nosniff Server: Aleph/0.4.1 Connection: Keep-Alive Date: Tue, 05 Jul 2016 15:41:36 GMT
It is! So let’s try it.
$ curl -i http://localhost:3000/hello-atom -X PUT -d "value=Hello Wonderful World!%0a"
And now let’s see if we’ve managed to change the state of the resource.
$ curl -i http://localhost:3000/hello-atom
HTTP/1.1 200 OK X-Frame-Options: SAMEORIGIN X-XSS-Protection: 1; mode=block X-Content-Type-Options: nosniff Content-Length: 23 Content-Type: text/plain;charset=utf-8 Last-Modified: Tue, 05 Jul 2016 16:08:22 GMT Vary: accept-charset ETag: 3c3e0684be182b7185f6ad10b63f246a Server: Aleph/0.4.1 Connection: Keep-Alive Date: Tue, 05 Jul 2016 16:08:35 GMT Hello Wonderful World!
As long as someone else hasn’t sneaked in a different state between your PUT
and subsequent GET
, you should see the new state of the resource is "Hello Wonderful World!". Great!
But what if someone did manage to PUT
their change ahead of yours? Their version would now be overwritten. That might not be what you wanted. To ensure we don’t override someone’s change, we could have set the If-Match header using the ETag value.
Let’s test this now, using the ETag value we got before we sent our PUT
request.
$ curl -i http://localhost:3000/hello-atom -X PUT -H "If-Match: fa863bd7ff53786d286e4bb3c0134416" -d "value=Hello Wonderful World!%0a"
HTTP/1.1 412 Precondition Failed Content-Length: 196 Content-Type: application/json Server: Aleph/0.4.1 Connection: Keep-Alive Date: Tue, 05 Jul 2016 16:10:53 GMT
We get a 412, which means a pre-condition failed. The pre-condition in question relates to our If-Match
header value not matching the current value of the atom. This is a very useful result, because it means we can ensure that we don’t overwrite someone else’s data.
There was one more method indicated by the Allow header of our OPTIONS
request, which was HEAD
. Let’s try this now.
Use the option --head to curl to tell it to issue a HEAD request (and not to expect a request body). |
$ curl -i --head http://localhost:3000/hello
The response does not have a body, but tells us the headers we would get if we were to try a GET
request.
Often, a resource’s state or behavior will depend on parameters in the request. Let’s say we want to pass a parameter to the resource, via a query parameter.
To show this, we’ll write some real code:
(require '[yada.yada :refer [yada resource]])
(defn say-hello [ctx]
(str "Hello " (get-in ctx [:parameters :query :p]) "!\n"))
(def hello-parameters-resource
(resource
{:methods
{:get
{:parameters {:query {:p String}}
:produces "text/plain"
:response say-hello}}}))
(def handler (yada/handler hello-parameters-resource))
This declares a resource with a GET method, which responds with a plain-text message formed from the query parameter.
Let’s see this in action, but without a parameter:
$ curl -i http://localhost:3000/hello-parameter
Here we get a 400 response. This means we’ve done something wrong (we’ve forgotten to add the query parameter).
Now let’s add the query parameter to the URI:
$ curl -i http://localhost:3000/hello-parameter?p=Ken
This should now get the 200 response we wanted:
HTTP/1.1 200 OK X-Frame-Options: SAMEORIGIN X-XSS-Protection: 1; mode=block X-Content-Type-Options: nosniff Content-Length: 11 Content-Type: text/plain Server: Aleph/0.4.1 Connection: Keep-Alive Date: Tue, 05 Jul 2016 16:23:26 GMT Hello Ken!
Great!
As well as query parameters, yada supports path parameters, request headers, form data, cookies and request bodies. You can have optional parameters, in fact, anything that can be expressed in Plumatic Schema, and yada will even coerce parameters to a range of types. For more details, see the parameters chapter.
Now we have seen how to build a single web resource, let’s see how to build a Swagger description from a collection of web resources.
In your editor, switch to src/edge/web_server.clj
. This file defines the overall route structure which includes our routes for "Hello World!". This has been included twice, both at the root and under the /api
path.
This second version uses the Clojure threading macro →
which wraps the route structure with yada/swaggered
and gives it a bidi tag (used for generating URIs, we’ll use this later).
[
;; Hello World!
(hello-routes {})
["/api" (-> (hello-routes {})
;; Wrap this route structure in a Swagger
;; wrapper. This introspects the data model and
;; provides a swagger.json file, used by Swagger UI
;; and other tools.
(yada/swaggered
{:info {:title "Edge API"
:version "1.0"
:description "An example API"}
:basePath "/api"})
;; Tag it so we can create an href to this API
(tag :edge.resources/api))]]
The purpose of yada/swaggered
is to augment the route structure given to it with a route to swagger.json
, which responds with a Swagger description of the route structure in JSON. Since yada resources are data maps, this is a relatively simple data transformation of the route structure.
We can test the resource is available at its /api
location with curl:
$ curl -i http://localhost:3000/api/hello
We can also query the Swagger description with curl:
$ curl -i http://localhost:3000/api/swagger.json
This time we get a JSON body returned:
HTTP/1.1 200 OK X-Frame-Options: SAMEORIGIN X-XSS-Protection: 1; mode=block X-Content-Type-Options: nosniff Content-Length: 290 Content-Type: application/json Last-Modified: Wed, 22 Jun 2016 15:45:16 GMT Vary: accept-charset ETag: 7833a69510d2b80f2a414c3c4ef2b4d4 Server: Aleph/0.4.1 Connection: Keep-Alive Date: Wed, 22 Jun 2016 15:56:25 GMT {"swagger":"2.0","info":{"title":"Edge API","version":"1.0","description":"An example API"},"produces":["application/json"],"consumes":["application/json"],"paths":{"/hello":{"get":{"produces":["text/plain"],"responses":{"default":{"description":""}}}}},"basePath":"/api","definitions":{}}
Notice we still get a Vary
header telling us that multiple charsets are available. JSON bodies are available in UTF-16 and UTF-32. Compare this with Clojure’s EDN, which is specificed to be UTF-8 only. In fact, yada is happy to produce Swagger definitions in EDN too:
$ curl -i http://localhost:3000/api/swagger.edn
Note that this time we get no Vary
header, since there are no charset alternatives.
HTTP/1.1 200 OK X-Frame-Options: SAMEORIGIN X-XSS-Protection: 1; mode=block X-Content-Type-Options: nosniff Content-Length: 284 Content-Type: application/edn Last-Modified: Wed, 22 Jun 2016 15:45:16 GMT ETag: 3aa57341aa88d68108dbead14f5b462c Server: Aleph/0.4.1 Connection: Keep-Alive Date: Wed, 22 Jun 2016 15:59:26 GMT {:swagger "2.0", :info {:title "Edge API", :version "1.0", :description "An example API"}, :produces ["application/json"], :consumes ["application/json"], :paths {"/hello" {:get {:produces ("text/plain"), :responses {:default {:description ""}}}}}, :basePath "/api", :definitions {}}
It’s these little details that yada takes care of for you. There is no trickery involved, it’s simply the result of an almost obsessive focus on the relevant web standards. There is nothing special about strings, yada applies the same logic for anything else you ask it to handle. We’ll see more in the next chapter.
By the way, if you want to see the Swagger UI, browse to http://localhost:3000/swagger/?url=http://localhost:3000/api/swagger.json
This has been a long chapter, but we have only covered a simple "Hello World!" example.
You should now realise that implementing even a basic example that properly complies with the HTTP standard is a surprisingly tough undertaking. But this simple example demonstrated how a rich and functional HTTP resource can be created with a tiny amount of code, and none of the behaviour we have seen is hardcoded or contrived. We have only demonstrated a simple Java string, and yada includes similar support for many other basic types (atoms, Clojure collections, files, directories, Java resources…).
But the best thing is you can programmatically create your own resources and types to fit your particular requirements.
Without a library like yada we would need to read and understand hundreds of pages of the HTTP RFCs and spend a great deal of extra effort coding up its various aspects. Of course, nobody would bother doing that, but the consequence is that we miss out on the many architectural benefits of HTTP.
Rarely can client code make any assumptions that the HTTP API it is accessing complies with the text in the HTTP RFCs, and must therefore rely on detailed knowledge of how the API is written, either through documentation, Swagger definitions, close collaboration between development teams or some other means (trial-and-error). This is a problem because it causes rigidity between our systems.
By using yada, we are pushing the responsibility of implementing HTTP correctly away from the programmer and into a library.
Can you improve this documentation? These fine people already did:
Malcolm Sparks, Joshua Ballanco, mike, Martin Klepsch & Lucas LagoEdit on GitHub
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close