This is a tour of the CloudKit REST/HTTP 1.1 API using curl. For the complete spec, see the CloudKit REST API.

If you haven't already installed the gem:

$ gem install cloudkit

If you already have the gem, make sure you're running the latest version (0.11.2):

$ gem list cloudkit
cloudkit (0.10.0) <-- need to upgrade
$ gem update cloudkit
$ gem list cloudkit
cloudkit (0.11.2, 0.10.0) <-- 0.11.2 is now in the list

Create a rackup file named config.ru, containing these two lines of code:

require 'cloudkit'
expose :notes

Run the app:

$ rackup config.ru

CloudKit is discoverable from top to bottom. Let's see what resource collections we're hosting:

$ curl -i http://localhost:9292/cloudkit-meta
HTTP/1.1 200 OK
ETag: "ef2f29b1834ef8c2bf0d8f1abb100177"
Cache-Control: proxy-revalidate
Content-Type: application/json
Content-Length: 20

{"uris":["\/notes"]}

See what we can do with these note resources:

$ curl -i -XOPTIONS http://localhost:9292/notes
HTTP/1.1 200 OK
Content-Length: 0
Content-Type: application/json
Allow: GET, HEAD, POST, OPTIONS

List the currently available notes:

$ curl -i http://localhost:9292/notes
HTTP/1.1 200 OK
ETag: "ffc5e6012614d759283199e67f79071b"
Link: <http://localhost:9292/notes/_resolved>; rel="http://joncrosby.me/cloudkit/1.0/rel/resolved"
Cache-Control: proxy-revalidate
Content-Type: application/json
Content-Length: 32

{"uris":[],"total":0,"offset":0}

Along with the usual metadata, many responses also provide discovery information via Link Headers as shown above. These links allow user agents to find related resources. The purpose of the above rel="resolved" Link Header is to offer a complete representation of all documents in a collection as an alternative to the simple list of links provided above. We'll look at the the result of using the "resolved" URI after we have created a few resources.

Let's move on, creating a note using POST:

$ curl -i -XPOST -d'{"title":"projects"}' http://localhost:9292/notes
HTTP/1.1 201 Created
Cache-Control: no-cache
Location: http://localhost:9292/notes/0dda06f0-b134-012b-a2d8-0017f2c62348
Content-Type: application/json
Content-Length: 159

{
"uri":"\/notes\/0dda06f0-b134-012b-a2d8-0017f2c62348",
"ok":true,
"etag":"0dda0de0-b134-012b-a2d8-0017f2c62348",
"last_modified":"Sun, 21 Dec 2008 02:21:52 GMT"
}

Create a different note using PUT so that we can specify its location:

$ curl -i -XPUT -d'{"title":"reminders"}' http://localhost:9292/notes/abc
HTTP/1.1 201 Created
Cache-Control: no-cache
Location: http://localhost:9292/notes/abc
Content-Type: application/json
Content-Length: 126

{
"uri":"\/notes\/abc",
"ok":true,
"etag":"89487620-b134-012b-a2d8-0017f2c62348",
"last_modified":"Sun, 21 Dec 2008 02:25:19 GMT"
}

View the new note:

$ curl -i http://localhost:9292/notes/abc
HTTP/1.1 200 OK
Last-Modified: Sun, 21 Dec 2008 02:25:19 GMT
ETag: "89487620-b134-012b-a2d8-0017f2c62348"
Link: <http://localhost:9292/notes/abc/versions>; rel="http://joncrosby.me/cloudkit/1.0/rel/versions"
Cache-Control: proxy-revalidate
Content-Type: application/json
Content-Length: 21

{"title":"reminders"}

Once again, we see a Link header. This one lists the location of the complete history of this particular document. This history contains all versions of the document including the most recent. We will see it in action in a moment.

Next, attempt a careless update of our newest resource and enjoy the failure:

$ curl -i -XPUT -d'{"title":"foo"}' http://localhost:9292/notes/abc
HTTP/1.1 400 Bad Request
Cache-Control: no-cache
Content-Type: application/json
Content-Length: 25

{"error":"etag required"}

Succeed in updating by being specific:

$ curl -i -XPUT -H'If-Match:89487620-b134-012b-a2d8-0017f2c62348' -d'{"title":"foo"}' http://localhost:9292/notes/abc
HTTP/1.1 200 OK
Cache-Control: no-cache
Content-Type: application/json
Content-Length: 126
Connection: keep-alive

{
"uri":"\/notes\/abc",
"ok":true,
"etag":"522be9f0-b135-012b-a2d8-0017f2c62348",
"last_modified":"Sun, 21 Dec 2008 02:30:56 GMT"
}

(Note: Your ETag will likely be different so substitute the one that curl provided when you created your own "abc" resource.)

Watch a secondary, out-of-date client fail at updating by being specific but also being stale:

$ curl -i -XPUT -H'If-Match:89487620-b134-012b-a2d8-0017f2c62348' -d'{"title":"bar"}' http://localhost:9292/notes/abc
HTTP/1.1 412 Precondition Failed
Cache-Control: no-cache
Content-Type: application/json
Content-Length: 31

{"error":"precondition failed"}

A 412 code is returned indicating a precondition for this request failed. Specifically, the ETag was out of date. In this case, our second client can fall back on the resource's history to "catch up" and apply its changes to the most recent version of the resource.

We can list all versions of the document using the URI tagged with rel="versions" in the link header mentioned above, reverse sorted by Last-Modified, feed style:

$ curl -i http://localhost:9292/notes/abc/versions
HTTP/1.1 200 OK
Last-Modified: Sun, 21 Dec 2008 02:30:56 GMT
ETag: "28ecf6899a45d3cdd0ad82bad56991d1"
Link: <http://localhost:9292/notes/abc/versions/_resolved>; rel="http://joncrosby.me/cloudkit/1.0/rel/resolved"
Cache-Control: proxy-revalidate
Content-Type: application/json
Content-Length: 109

{
"uris":[
"\/notes\/abc",
"\/notes\/abc\/versions\/89487620-b134-012b-a2d8-0017f2c62348"
],
"total":2,
"offset":0
}

List all versions again, this time using the "resolved" URI from the Link header. This effectively delivers the same information that would be obtained by first listing the URIs, then fetching each one of them individually.

$ curl -i http://localhost:9292/notes/abc/versions/_resolved
HTTP/1.1 200 OK
Last-Modified: Sun, 21 Dec 2008 02:30:56 GMT
ETag: "282819afc09d7735fd6801532c0c7033"
Link: <http://localhost:9292/notes/abc/versions>; rel="index"
Cache-Control: proxy-revalidate
Content-Type: application/json
Content-Length: 390

{
"documents":[
{
"etag":"522be9f0-b135-012b-a2d8-0017f2c62348",
"last_modified":"Sun, 21 Dec 2008 02:30:56 GMT",
"document":"{\"title\":\"foo\"}",
"uri":"\/notes\/abc"
},
{
"etag":"89487620-b134-012b-a2d8-0017f2c62348",
"last_modified":"Sun, 21 Dec 2008 02:25:19 GMT",
"document":"{\"title\":\"reminders\"}",
"uri":"\/notes\/abc\/versions\/89487620-b134-012b-a2d8-0017f2c62348"}
],
"total":2,
"offset":0
}

Notice the resolved response includes a Link header pointing back to its index.

We can use this same "resolved" technique on the main "notes" listing:

$ curl -i http://localhost:9292/notes/_resolved
HTTP/1.1 200 OK
Last-Modified: Sun, 21 Dec 2008 02:30:56 GMT
ETag: "6628242625a7f71cce838a02deb27912"
Link: <http://localhost:9292/notes> rel="index"
Cache-Control: proxy-revalidate
Content-Type: application/json
Content-Length: 374

{
"documents":[
{
"etag":"522be9f0-b135-012b-a2d8-0017f2c62348",
"last_modified":"Sun, 21 Dec 2008 02:30:56 GMT",
"document":"{\"title\":\"foo\"}",
"uri":"\/notes\/abc"
},
{
"etag":"0dda0de0-b134-012b-a2d8-0017f2c62348",
"last_modified":"Sun, 21 Dec 2008 02:21:52 GMT",
"document":"{\"title\":\"projects\"}",
"uri":"\/notes\/0dda06f0-b134-012b-a2d8-0017f2c62348"
}
],
"total":2,
"offset":0
}

Next, let's delete the our most recent document:

$ curl -i -XDELETE -H'If-Match:522be9f0-b135-012b-a2d8-0017f2c62348' http://localhost:9292/notes/abc
HTTP/1.1 200 OK
Cache-Control: no-cache
Content-Type: application/json
Content-Length: 174

{
"uri":"\/notes\/abc\/versions\/522be9f0-b135-012b-a2d8-0017f2c62348",
"ok":true,
"etag":"522be9f0-b135-012b-a2d8-0017f2c62348",
"last_modified":"Sun, 21 Dec 2008 02:30:56 GMT"
}

Try to GET it again and notice the helpful 410:

$ curl -i http://localhost:9292/notes/abc
HTTP/1.1 410 Gone
Link: <http://localhost:9292/notes/abc/versions>; rel="http://joncrosby.me/cloudkit/1.0/rel/versions"
Cache-Control: no-cache
Content-Type: application/json
Content-Length: 37

{"error":"entity previously deleted"}

Notice the history is preserved:

$ curl -i http://localhost:9292/notes/abc/versions
HTTP/1.1 200 OK
Last-Modified: Sun, 21 Dec 2008 02:30:56 GMT
ETag: "2308ee33e953c9be41221ff7612e5217"
Link: <http://localhost:9292/notes/abc/versions/_resolved>; rel="http://joncrosby.me/cloudkit/1.0/rel/resolved"
Cache-Control: proxy-revalidate
Content-Type: application/json
Content-Length: 157

{
"uris":[
"\/notes\/abc\/versions\/522be9f0-b135-012b-a2d8-0017f2c62348",
"\/notes\/abc\/versions\/89487620-b134-012b-a2d8-0017f2c62348"
],
"total":2,
"offset":0
}