Jester: JavaScriptian REST
Posted by Eric Mill
Apr 02
Jester is our implementation of REST, in JavaScript. It provides (nearly) identical syntax to ActiveResource for using REST to find, update, and create data, but from the client side.
Update, 6/16/07: We have released version 1.3 of Jester. You may want to view its release description.
Jester is available from SVN in trunk form, or a 1.1 release form. You can also download a zipped copy of 1.1. Jester is released under the MIT License.
Syntax
All examples below are taken from inside the JavaScript console of Firebug, the best JavaScript development tool you could possibly have.
First, declare a model in Jester by calling model on Base:1 2 3 |
>>> Base.model("User") >>> User Object _name=User _singular=user _plural=users |
1 2 3 |
>>> Base.model("Child", "http://www.thoughtbot.com", "child", "children") >>> Child Object _name=Child _singular=child _plural=children |
1 2 3 |
>>> var Child = new Base("Child", "http://www.thoughtbot.com", "child", "children") >>> Child Object _name=Child _singular=child _plural=children |
1 2 3 4 5 6 7 8 9 10 11 12 13 |
>>> eric = User.find(1) GET http://localhost:3000/users/1.xml Object _name=User _singular=user _plural=users >>> eric.attributes ["active", "email", "id", "name"] >>> eric.id 1 >>> eric.name "Eric Mill" >>> eric.active true |
1 2 3 4 5 6 7 8 9 10 |
>>> floyd = User.create({name: "Floyd Wright", email: "tfwright@thoughtbot.com"})
POST http://localhost:3000/users.xml
Object _name=User _singular=user _plural=users
>>> floyd.id
9
>>> User.find(9).name
GET http://localhost:3000/users/9.xml
"Floyd Wright" |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
>>> eric = User.find(1) GET http://localhost:3000/users/1.xml Object _name=User _singular=user _plural=users >>> eric.email "emill@thoughtbot.com" >>> eric.email = "sandybeach@wintermute.com" "sandybeach@wintermute.com" >>> eric.save() POST http://localhost:3000/users/1.xml true >>> User.find(eric.id).email GET http://localhost:3000/users/1.xml "sandybeach@wintermute.com" |
1 2 3 4 5 6 7 8 9 10 11 12 13 |
>>> chad = User.build({email: "cpytel@thoughtbot.com", name: "Chad Pytel"})
Object _name=User _singular=user _plural=users
>>> chad.new_record()
true
>>> chad.save()
POST http://localhost:3000/users.xml
true
>>> chad.id
9
>>> chad.new_record()
false |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
>>> jared = User.build({name: "", email: ""})
Object _name=User _singular=user _plural=users
>>> jared.save()
POST http://localhost:3000/users.xml
false
>>> jared.errors
["Name can't be blank", "Email can't be blank"]
>>> jared.valid()
false
>>> jared.name = "Jared Carroll"
"Jared Carroll"
>>> jared.email = "emill@thoughtbot.com"
"emill@thoughtbot.com"
>>> jared.save()
POST http://localhost:3000/users.xml
false
>>> jared.errors
["Email has already been taken"]
>>> jared.email = "jcarroll@thoughtbot.com"
"jcarroll@thoughtbot.com"
>>> jared.save()
POST http://localhost:3000/users.xml
true |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
>>> eric = User.find(1) GET http://localhost:3000/users/1.xml Object _name=User _singular=user _plural=users >>> eric.posts [Object _name=Post _singular=post _plural=posts, Object _name=Post _singular=post _plural=posts] >>> eric.posts.first().body "Today I passed the bar exam. Tomorrow, I make Nancy my wife." >>> eric.posts.first().body = "Today I *almost* passed the bar exam. The ring waits one more day." "Today I *almost* passed the bar exam. The ring waits one more day." >>> eric.posts.first().save() POST http://localhost:3000/posts/1.xml true >>> post = Post.find(1) GET http://localhost:3000/posts/1.xml Object _name=Post _singular=post _plural=posts >>> post.body "Today I *almost* passed the bar exam. The ring waits one more day." >>> post.user Object _name=User _singular=user _plural=users >>> post.user.name "Eric Mill" |
Using Jester
Jester depends on two libraries: Prototype, which comes with Rails and most people are familiar with, and ObjTree, a nice DOM parsing engine for JavaScript. Both of these are packaged along with Jester in its SVN repository, so you don’t have to hunt for them yourself. Just make sure you’re including all three in your test file.
<script type="text/javascript" src="/javascripts/prototype.js"></script>
<script type="text/javascript" src="/javascripts/ObjTree.js"></script>
<script type="text/javascript" src="/javascripts/jester.js"></script>
JavaScript in the browser is limited to requests with in only the same domain as the script is running in, so without iframe hackery, Jester is probably only useful for writing client code in your own apps, to talk to itself. We’re investigating whether Jester can use this hackery to make cross-domain requests, but it’s not clear if this will be feasible.
There are also some basic unit tests included inside Jester’s repository, which run using JsUnit. To run them yourself, from Jester’s repository open the file test/jsunit/testRunner.html in your browser, and choose test/jester_test.html as the test file.
The Server Side
These examples are talking with a Rails application whose controllers were generated with ”./script generate scaffold_resource”—in other words, the ideal RESTful controllers. It’s very easy to make your controller RESTful. Here’s the source for the User controller I’m using. The lines that deal with returning HTML have been removed, and I have added “(:include => :posts)” as an argument to to_xml in two places, so associations are included (it’s that easy!).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
class UsersController < ApplicationController # GET /users.xml def index @users = User.find(:all) respond_to do |format| format.xml { render :xml => @users.to_xml(:include => :posts) } end end # GET /users/1.xml def show @user = User.find(params[:id]) respond_to do |format| format.xml { render :xml => @user.to_xml(:include => :posts) } end end # POST /users.xml def create @user = User.new(params[:user]) respond_to do |format| if @user.save format.xml { head :created, :location => user_url(@user) } else format.xml { render :xml => @user.errors.to_xml } end end end # PUT /users/1.xml def update @user = User.find(params[:id]) respond_to do |format| if @user.update_attributes(params[:user]) format.xml { head :ok } else format.xml { render :xml => @user.errors.to_xml } end end end # DELETE /users/1.xml def destroy @user = User.find(params[:id]) @user.destroy respond_to do |format| format.xml { head :ok } end end end |
<user>
<active type="boolean">true</active>
<email>cpytel@thoughtbot.com</email>
<id type="integer">2</id>
<name>Chad Pytel</name>
<posts>
<post>
<title>Life as a Jester</title>
<body>It's not as hard as Master said it would be. Today I made 200 dollars.</body>
<created-at type="datetime">2007-04-01T04:01:56-04:00</created-at>
<id type="integer">2</id>
<user-id type="integer">2</user-id>
</post>
</posts>
</user>
To see some real live examples, the Beast forum is currently implenting some of ActiveResource. Here’s technoweenie’s User account, in XML, and an XML list of selected Users. Pretty much any URL in Beast can have ”.xml” appended to it.
ActiveResource Reference
Taking ARes Out for a Test Drive—Great introduction to ActiveResource, by one of its authors.
ActiveResource and Testing—A post I made here discussing how I tested ActiveResource models.
ActiveResource’s Subversion Repository—Current ActiveResource trunk, at svn.rubyonrails.org.
Thanks
Thanks go to Chad for the original idea, and Jared for writing Jester’s tests.
Jester is new, so we’d love to hear feedback on its strengths and weaknesses. We’re using it ourselves, so it’s under active development and getting plenty of love and attention. Please tell us what you think!
Comments on this post
Apr 02
rick said,
So, how do you deal with prefixes? You added a posts association on User? I added a prefix_options hash in ActiveResource so I can post a new post to /users/1/posts. Not a great solution, I have some ideas on making it a bit smarter though.
Apr 02
Dr Nic said,
That looks beautiful!
Apr 02
Alex MacCaw said,
Looks brilliant, I assume all requests are made synchronously? Also, why not use json, much quicker than xml and easily implemented on the server side.
Apr 02
Eric Mill said,
Rick, I was thinking about prefixes while writing this—I’m unsure of how to go about them. This isn’t a complete answer, but I might actually be able to get away with something like this:Apr 02
Eric Mill said,
Alex—yes, all requests are synchronous. It wouldn’t be too hard to take an optional callback argument to find, create, save, and destroy, and have them operate asynchronously though. I might do that.
As for JSON/XML—we modeled this after ActiveResource, using its conventions, so Jester is geared towards working with XML. If there are JSON conventions that Jester could rely on, then supporting it would be feasible. Do you know of any resources I could look at?
Apr 02
Alex MacCaw said,
Eric - You’re probably right – it was just wishful thinking ;). the .to_json method doesn’t even render active record objects correctly anyway, I’ve had to find a hacky method: http://www.eribium.org/?p=70
Apr 02
Alex MacCaw said,
Also, regarding synchronicity, I think making it async is invaluable, I would think 99% of web apps do it this way so as not to lock up.
Apr 02
Eric Mill said,
I was just talking with a friend who felt the same way. I agree. Here’s the syntax I think I’ll implement:
// synchronous find User.find(1) // asynchronous find, calls callbackFunction on complete User.find(1, callbackFunction) // asynchronous find, merges optionsHash into Jester's default Ajax.Request hash and passes directly to Prototype User.find(1, {onSuccess: yesCallback, onFailure: noCallback})Does that sound about right?
Apr 02
Alex MacCaw said,
Sounds great, I’m also wondering if you’re going to implement finding with conditions. This is how I’m doing it in actionscript, shouldn’t be too difficult to port:protected function parse_conditions(con:*):String { var st:String = new String(); if (con is Array) { var r:Array = new Array(); for ( var i:Number=0 ;i < con.length; i ++){ r.push('conditions[]=' + URI.escapeChars(con[i])); } st = r.join('&') } else { st = 'conditions=' + URI.escapeChars(con) } return st; }Apr 02
Jon said,
Looks great, but I would STRONGLY advise against switching to JSON at this time. XML is much more secure. http://www.cbronline.com/article_news.asp?guid=484BC88B-630F-4E74-94E9-8D89DD0E6606
Apr 04
Tim Lucas said,
Jon: that article is complete rubbish. They’re just referring to XSS, or cross-site scripting, to which most web apps are vulnerable but app developers choose to ignore.
Apr 04
Colin Macdonald said,
Nice stuff Eric. Count another vote for asynchronous.
@Jon – Argh. It doesn’t make a difference what your data format is. Look at you transport layer.
I’m also in favour of a JSON implementation, mainly because it’s just simpler and easier to use (IMHO).
Apr 04
Alex MacCaw said,
Also, what about find(:all), I can only find ‘find’ for ids?
Apr 04
femto said,
how about User.new()? I would prefer User.new() over User.build
Apr 04
Cleveland said,
Good point Femto.
Eric – when do you expect the browser landscape to evolve? Are you in meetings with any major browser vendors?
Apr 04
Eric Mill said,
Cleveland – Haha, I was just trying to say “when IE catches up to Firefox” in a polite way. We’ll see about IE8.
Femto – Fortunately, I can see the actual text of comments! You mean prefacing new() with underscores. I can provide an underscored new as an alternative, for sure. They’ll both be available.
Apr 04
femto said,
yes, I put two underscores before new, but in renders into italics..
Apr 04
Phillip Bogle said,
“Without iframe hackery, Jester is probably only useful for writing client code in your own apps, to talk to itself. We’re investigating whether Jester can use this hackery to make cross-domain requests, but it’s not clear if this will be feasible.”
Rather than struggling with IFrame hackery, it’s often better to simply set up a simple proxy on your web server that forwards requests from clients to a set of trusted services.
This Yahoo developer article has one example of a PHP proxy as well as some security concerns to take into account:
http://developer.yahoo.com/javascript/howto-proxy.html
Apr 05
Caleb Jones said,
Good job! Very clean and true syntax. But as you promote this library, please, please, please be very up front about warning people that implementing this with no user authentication they are effectively making available to the world an open connection to their database. I can see users new to web development glossing over the necessary steps to securely using this library.
Apr 05
Tammer Saleh said,
Caleb: This is a valid concern with any rails application, but not one that jester aggravates.
Since jester interacts with the application through the normal REST interface, any session based security in the controllers will still apply. So, if you’re on the /users/1;edit page, logged in as user 1, Jester will contain the session[user_id] parameter as normal, and jester calls will authenticate with that.
Apr 05
Eric Mill said,
Right – cookies will be sent with XMLHttpRequests just like every other GET/POST request to the server. So, as long as you write your controllers to authenticate, Jester poses no problem.
The controller example I showed uses no authentication, it’s the default controller generated as a “scaffold_resource” by Rails. In practice, you would not want to expose your models this completely.
Apr 06
Kris Zyp said,
Could this be adapted to use JSPON instead of XML? JSPON is a format that extends JSON to facilitate persistent object transfers.
Apr 08
Dan Kubb said,
Eric, this is absolutely fantastic. This (and a few other cool things this week) is enough to get me to switch back to Prototype.
+1 for an async option too.
Also, you may want to look at the bottom of ActiveResource’s README file for a list of error codes a RESTful Rails app can return. I believe the status codes Jester is checking aren’t quite in-line with ARs’ behaviour. For example a status between 200-399 is considered to not have returned an error, while 422 is used when the app receives bad data.
Apr 08
Snoop Baron said,
This is exactly what I was looking for after seeing DHH’s CRUD presentation, great stuff guys :).
Apr 10
__sam__ said,
Good job guys ! I think an interesting feature would be to add some reflection to models, as we may not know by advance their structure. As you can call
object.attributesto know its attributes, it would be nice to have aobject.relationsor something to discover its relations (I’m posting this ticket to ActiveResource too…).Another point : applications using Jester would probably have complex structure and a need of inheritance. Maybe it’s just time to look after a good implementation of inheritance like http://twologic.com/projects/inheritance/
Apr 11
Erik Allik said,
It seems that Jester does not work in XUL applications at the moment.
MyModel.find(id) causes an exception “doc has no properties” on line 96 of jester.js.
Excuse me if this is not the right place to post this.
Apr 11
Chad Pytel said,
Erik, This is indeed a fine place to post this. Interestingly, XULrunner is one of the exact places we plan on using Jester, but admittedly, we have not yet begun to use it outside of a normal web page. We should be using it inside our XUL app in the next few days, in the meantime, if you happen to spot the fix, please let us know. Thanks.
Apr 19
nap said,
This is great stuff. Thanks.
Sorry, comments are closed for this article.
© 2000 - 2009 by thoughtbot, inc.
written by a bushel of tiny robots
Come “ride the toad” on Hoptoad, the app error app.
Thunder Thimble: Brand monitoring for social media.
Widgetfinger: Simple content management for simple websites.
Tee-Bot, funny shirts your friends won't understand!
Umbrella Today: “It’s like totally the simplest weather report ever, Julie.”
Thoughtbot
thoughtbot is a technology consulting firm that provides web application development and design services. We focus on building modern systems, embracing good ideas and delivering elegant solutions.
Interested in learning Rails?
Sign up for our beginning or advanced training.
Archives