• If you are citizen of an European Union member nation, you may not use this service unless you are at least 16 years old.

  • You already know Dokkio is an AI-powered assistant to organize & manage your digital files & messages. Very soon, Dokkio will support Outlook as well as One Drive. Check it out today!

View
 

Todos 06-Building with Compojure and MongoDB

Page history last edited by Tero Parviainen 13 years, 9 months ago

About Clojure

 

Clojure is a dynamic programming language that targets the Java Virtual Machine. It is designed to be a general-purpose language, combining the approachability and interactive development of a scripting language with an efficient and robust infrastructure for multithreaded programming.
 

About Compojure

 

Compojure is a small, open source web framework for the Clojure programming language.

 

About MongoDB

 

MongoDB (from "humongous") is a scalable, high-performance, open source, document-oriented database
 

Prerequisites

 

Make sure you have installed a recent Java SDK, Leiningen, and MongoDB. There's no need to install Clojure if you haven't already, since Leiningen will take care of that.

 

Source Code

 

The full source code for this project is available on GitHub, so feel free to grab that if you don't feel like doing it all by hand.

 

Create the project

 

We'll create a small Compojure application that will serve as a backend for the Todos application. The application data will be stored in MongoDB.

 

Create a new Todos server app by entering the following command into the console:

 

$ lein new todos-server
$ cd todos-server
 

Make sure that you do issue this command outside your SproutCore application folder!

 

Add Compojure and MongoDB support to the application

 

Open up project.clj in the project root directory, and add dependency declarations for Compojure and the Congomongo MongoDB adapter for clojure, as well as the Ring Jetty adapter, which we'll use to run the server.

 

The file should look something like this:

 

(defproject todos-server "1.0.0-SNAPSHOT"
  :description "Backend for the SproutCore tutorial app"
  :dependencies [[org.clojure/clojure "1.1.0"]
                 [org.clojure/clojure-contrib "1.1.0"]
                 [compojure "0.4.0-RC3"]
                 [congomongo "0.1.2-SNAPSHOT"]]
  :dev-dependencies [[ring/ring-jetty-adapter "0.2.0"]])
 

Now run the following command:

 

$ lein deps

 

This will download and copy some libraries to your lib/ directory.

 

Fetching Records

 

We'll divide the app into two namespaces - one for database access and one for the REST API itself. First, add a file called src/todos_server/db.clj. This is where we'll put our database access code.

 

Add the following code to the new file:

 

(ns todos-server.db
 (:use somnium.congomongo))
 
(mongo! :db "todos")
 
(defn find-all-tasks []
  (fetch :tasks))
 

Here we are pulling in the Congomongo library and setting up a connection to a local MongoDB database called "todos". Then, we're defining a function for loading our tasks. This function returns a Clojure data structure of all of the tasks in the database.

 

Now, let's add the Compojure handler for returning a JSON response of these tasks. Create a file called src/todos_server/api.clj. This is where we'll put our REST API. Let's add our namespace declaration, some helper functions, and our route mapping to this file:

 

(ns todos-server.api
  (:use clojure.contrib.json.write
        clojure.contrib.json.read
        clojure.contrib.duck-streams
        compojure.core
        todos-server.db))
 
(defn- emit-json
  "Turn the object to JSON, and emit it with the correct content type"
  [x]
  {:headers {"Content-Type" "application/json"}
   :body    (json-str {:content x})})
 
(defn task-path
  "Returns the relative URL for task"
  [task]
  (str "/tasks/" (:_id task)))
 
(defn with-guid
  "Associates task with :guid pointing to its relative URL"
  [task]
  (assoc task :guid (task-path task)))
 
(defroutes main-routes
  (GET "/tasks" []
    (emit-json
      (map with-guid (find-all-tasks)))))
 

The main-routes definition here is the entry point to the application. There we map GET requests matching to the "/tasks" path, and define the handler for those requests. The handler calls the find-all-tasks function in our db namespace. Then it associates each task returned by that function with a :guid key which points to the tasks URL (this is what SproutCore expects). Finally, the tasks are serialized into a JSON string by calling the emit-json function.

 

Now we have all we need for fetching tasks from the database. But we still need to implement our retrieve, create, update, and destroy actions. Let's do that now.

 

Finalize the Database API

 

Back in db.clj, add the following function:

 

(defn find-task [id]
  (fetch-one :tasks :where {:_id id}))
 

This function retrieves a single task from the database by its id. Pretty straightforward.

 

Next, let's add support for task creation:

 

(defn- uuid []
  (str (java.util.UUID/randomUUID)))
 
(defn add-task [task]
  (insert! :tasks (assoc task :_id (uuid))))
 

Here we defined a helper function for generation ids for our tasks. We are not using MongoDBs internal ObjectIds here, so we need to come up with our own ids. Thankfully its easy to reach into the Java UUID class, which does just that.

 

The add-task function takes a task, associates it with one of those newly generated ids, and inserts it to the tasks collection in our database.

 

Next, task updates. This is slightly more complicated than what we've done before:

 

(defn keywordify-keys
  "Returns a map otherwise same as the argument but
   with all keys turned to keywords"
  [m]
  (zipmap
    (map keyword (keys m))
    (vals m)))
 
(defn merge-with-kw-keys
  "Merges maps converting all keys to keywords"
  [& maps]
  (reduce
    merge
    (map keywordify-keys maps)))
 
(defn update-task [id task]
  (let [task-in-db (find-task id)]
    (update! :tasks
      task-in-db
      (merge-with-kw-keys task-in-db task))))
 

What we need to do here, is fetch an existing task from the database, merge it with the task we've received, and finally save the result back into the database. The tricky part is that we might get string keys ("description") from the client, and our database API deals with keyword keys (:description), so we need to do some conversion to get the merge right. This is what the keywordify-keys and merge-with-kw-keys functions are all about.

 

Finally, task deletion. Thankfully this is pretty simple again:

 

(defn destroy-task [id]
  (destroy! :tasks
    (find-task id)))
 

All we do is find the task we want to delete, and then tell the database to destroy it.

 

Now our database API is complete. The final version should look something like this.

 

Adding Test Data

 

You can play with the database API by launching a Clojure REPL on the command line with the following command:

 

$ lein repl
 

Once the REPL has launched, you can pull in the db namespace and start adding some test data:

 

=> (use 'todos-server.db)
=> (add-task {:description "Finding Clojure" :done false})
{:_id "cc582085-34bf-4bc5-91f3-27320a38c620", :description "Finding Clojure", :done false, :_ns "tasks"}
=> (find-all-tasks)
({:_id "cc582085-34bf-4bc5-91f3-27320a38c620", :description "Finding Clojure", :done false, :_ns "tasks"})
 

Finalize the REST API

 

Next, let's finalize our API namespace. First, we'll need one more helper function in api.clj:

 

(defn- parse-json
  "Parse the request body into a Clojure data structure"
  [body]
  (read-json (slurp* body)))
 

This function reads an incoming request stream containing JSON data, and parses it into a Clojure data structure.

 

Now, we're ready to add the rest of our routes. This is what the final route map should look like:

 

(defroutes main-routes
 
  (GET    "/tasks" []
    (emit-json
      (map with-guid (find-all-tasks))))
 
  (GET    "/tasks/:id" [id]
    (emit-json
      (with-guid (find-task id))))
 
  (POST   "/tasks" {body :body}
    (let [saved-task (add-task (parse-json body))]
      {:status 201
       :headers {"Location" (task-path saved-task)}}))
 
  (PUT    "/tasks/:id" {body :body {id "id"} :route-params}
    (update-task id (parse-json body)))
 
  (DELETE "/tasks/:id" [id]
    (destroy-task id)
    {:status 200}))
 

Each route mapping is followed by a Clojure structural binding that extracts the information we need from the request, and a handler definition that executes the operation, and returns a response (except for PUT, where we're only concerned about returning a successful HTTP status code since that's the only thing the client cares about).

 

The final API should look like this.

 

Start the server

 

Now we're ready to start a Jetty server that serves our API. If you haven't already, launch a Clojure REPL for the project:

 

$ lein repl
 

In the REPL, pull in the Ring Jetty adapter, and our API namespace, and then launch Jetty:

 

=> (use 'ring.adapter.jetty)
=> (use 'todos-server.api)
=> (run-jetty main-routes {:port 8080})
 

You can check that the server as indeed running and serving your test data by loading http://localhost:8080/tasks

 

Setup your proxy

 

Add the following line to the Buildfile file in your SproutCore project:

 

proxy "/tasks", :to => "localhost:8080"
 

You’re Done!

 

Continue to next step: Step 7: Hooking Up to the Backend

 

Comments (1)

Dr. Baba Kofi Weusijana said

at 2:59 pm on Oct 19, 2010

Are some files missing from the source files? I keep getting in the "Adding Test Data" section:
lein repl
Warning: the repl task currently doesn't honor some project.clj
options due to I/O stream issues. Future versions will address
this, but for now you will get more consistent behaviour from repls
launched by either the lein-swank plugin or the lein-nailgun plugin.

Clojure 1.2.0
user=> (use 'todos-server.db)
java.io.FileNotFoundException: Could not locate somnium/congomongo__init.class or somnium/congomongo.clj on classpath: (db.clj:1)

You don't have permission to comment on this page.