Thomas Parslow
tom@almostobsolete.net
@almostobsolete
http://almostobsolete.net




The RESTful API
| Verb | Safe | Idempotent |
|---|---|---|
| GET | X | X |
| PUT | X | |
| DELETE | X | |
| POST |
Idempotent is a big word, but it just means that doing something lots is the same as doing it once. So automatic retries are fine!
{
"href": "http://example.com/api/v1/users/john",
}
{
"href": "http://example.com/api/v1/users/john",
"type": "user",
"head": {
...
},
"body": {
...
}
}
{
"href": "http://example.com/api/v1/users/john",
"type": "user",
"head": {
...
"related": {
"meetings": {
"href": "http://example.com/api/v1/users/john/meetings"
}
}
},
"body": {
...
}
}
{
"href": "http://example.com/api/v1/users/john",
"type": "user",
"head": {
"allow": ["GET","PUT","PATCH","DELETE"],
"created_at": "2011-03-09T00:33:00Z",
"updated_at": "2011-03-09T00:33:00Z",
"related": {
"meetings": {
"href": "http://example.com/api/v1/users/john/meetings"
}
}
},
"body": {
"display_name": "John Doe",
"phone_number": "123456789",
"email": "john@example.com",
}
}
GET http://example.com/api/v1/meetings/MEETINGID
RESPONSE:
HTTP 200 OK
{
"href": "http://example.com/api/v1/meetings/MEETINGID",
"type": "meeting",
"head": {...},
"body": {...}
}
PUT http://example.com/api/v1/meetings/MEETINGID
{
"body": {
"title": "My awesome meeting"
....
}
}
RESPONSE:
HTTP 200 OK
{
"href": "http://example.com/api/v1/meetings/MEETINGID",
"type": "meeting",
"head": {...},
"body": {
"title": "My awesome meeting"
....
}
}
DELETE http://example.com/api/v1/meetings/MEETINGID RESPONSE: HTTP 204 No Content
POST http://example.com/api/v1/users/USERID/meetings
{
"body": {
"title": "My new meeting"
....
}
}
RESPONSE:
HTTP 201 Created
Location: http://example.com/api/v1/meetings/NEWMEETINGID
ws://example.com/api/v1/meetings/MYMEETING
{
"type": "comment",
"href": "http://example.com/api/v1/meetings/MYMEETING/activities/99",
"head": {
"related": {
"parent": {
"href": "http://example.com/api/v1/meetings/MEETINGID/activities"
}
}
},
"body": {
"text": "Hello world!"
}
}
Example resource:
{
"type": "conference",
"href": "http://example.com/api/v1/conferences/CONFERENCEID",
"head": {
...
},
"body": {
"lecture_mode": false,
"recording": false,
"locked": false
}
}
JSONPatch to start recording:
PATCH http://example.com/api/v1/conferences/CONFERENCEID
[
{"replace": "/body/recording", "value": true},
]
Demo here: The API
A Quick Intro to Backbone.JS
TodoModel = Backbone.Model.extend({
defaults: {
done: false,
title: 'Todo item'
}
});
var model = new TodoModel({title: 'Do stuff'});
model.on('change:title', function () {
alert('Title is now: ' + model.get('title'));
});
model.set({title: 'Do other stuff'});
var TodoView = Backbone.View.extend({
events: {'click': 'updateDone'},
initialize: function () {
this.model.on('change', this.render, this);
},
render: function () {
this.$el.html('<input type="checkbox"/><b></b>');
this.$('input').attr('checked',this.model.get('done'));
this.$('b').text(this.model.get('title'));
},
updateDone: function () {
this.model.set('done', !this.model.get('done'));
}
});
var view = new TodoView({model: model});
view.render();
$('#todo-items').append(view.el);
var todoitems = new Backbone.Collection();
todoitems.on('add', function (added) {
alert('Todo item added with title: ' + added.get('title'));
});
todoitems.add(model);
JavaScript Client App
# Create a client instance (usually once per app)
client = new resources.Client({
base: "http://example.com/api/v1/"
});
# Get a resource from a known url. If the resource exists in the cache
# it will be returned.
meeting = client.getResource(
'http://example.com/api/v1/meetings/MEETINGID'
);
# Make sure the resource has been fetched from the server
# (only fetch if it hasn't already been fetched)
meeting.maybeFetch().done(function () {
# It's now safe to assume that the meeting is loaded
alert(meeting.get('title'));
});
# Subscribe to the WebSocket stream for this meeting meeting.subscribe();
# Follow a "related" hyperlink
comments = meeting.related('comments');
# Fetch each item in a collection. If there where inclusions
# instead of links this doesn't actually go to the server.
comments.fetchEach(function (comment) {
alert('I have a fully loaded comment here:' + comment.get('text'));
# Combines a call to related and to maybeFetch
comment.fetchRelated('owner', function (user) {
alert(user.get('name') + ' posted that comment');
});
});
# Actual version is in CoffeeScript
var MeetingView = views.BaseView.extend({
template: 'app/templates/meeting/meeting.html',
contextBindings: {
title: 'title',
starts_at: 'starts_at',
owner_name: 'owner/full_name'
},
...
});
<div class="meeting">
<h1>{{title}} at {{starts_at}} by {{owner_name}}</h1>
<ul class="comments">
</ul>
</div>
var MeetingView = views.BaseView.extend({
...
attachPoints: {
comment: '.comments'
}
});
var Comment = views.BaseView.extend({
...
});
# In a controller somewhere
mettingview.appendSubView(new Comment({model: commentModel}));
<div class="meeting">
<h1>{{title}} at {{starts_at}} by {{owner_name}}</h1>
<ul class="comments">
</ul>
</div>
| From | To | Handlers Called |
| / | /meeting/MEETINGID | enterMeeting(MEETINGID) |
| /meeting/MEETINGID | /meeting/MEETINGID/presentation | enterPresentation |
| /meeting/MEETINGID/presentation | /meeting/MEETINGID/asset/ASSETID |
leavePresentation enterAsset(ASSETID) |
| /meeting/MEETINGID/asset/ASSETID | / |
leaveAsset leaveMeeting |
| From | To | Handlers Called |
| / | /meeting/MEETINGID | enterMeeting(MEETINGID) |
| /meeting/MEETINGID | /meeting/MEETINGID/presentation | enterPresentation |
| /meeting/MEETINGID/presentation | /meeting/MEETINGID/asset/ASSETID |
leavePresentation enterAsset(ASSETID) |
| /meeting/MEETINGID/asset/ASSETID | / |
leaveAsset leaveMeeting |
| From | To | Handlers Called |
| / | /meeting/MEETINGID | enterMeeting(MEETINGID) |
| /meeting/MEETINGID | /meeting/MEETINGID/presentation | enterPresentation |
| /meeting/MEETINGID/presentation | /meeting/MEETINGID/asset/ASSETID |
leavePresentation enterAsset(ASSETID) |
| /meeting/MEETINGID/asset/ASSETID | / |
leaveAsset leaveMeeting |
| From | To | Handlers Called |
| / | /meeting/MEETINGID | enterMeeting(MEETINGID) |
| /meeting/MEETINGID | /meeting/MEETINGID/presentation | enterPresentation |
| /meeting/MEETINGID/presentation | /meeting/MEETINGID/asset/ASSETID |
leavePresentation enterAsset(ASSETID) |
| /meeting/MEETINGID/asset/ASSETID | / |
leaveAsset leaveMeeting |
| From | To | Handlers Called |
| / | /meeting/MEETINGID | enterMeeting(MEETINGID) |
| /meeting/MEETINGID | /meeting/MEETINGID/presentation | enterPresentation |
| /meeting/MEETINGID/presentation | /meeting/MEETINGID/asset/ASSETID |
leavePresentation enterAsset(ASSETID) |
| /meeting/MEETINGID/asset/ASSETID | / |
leaveAsset leaveMeeting |
base = new router.Route()
meetingroute = base.extend('/meeting/:id', {
enter: function (id) {
// Assign stuff to the context in the enter function...
this.view = createMeetingView();
},
leave: function () {
//...and it's available in the leave function too
this.view.destroy();
}
});
# full path would now be /meeting/:id/summary
meetingroute.extend('/summary', {
enter: function () {
# We still have access to the view we set in the parent route
this.view.showSummary();
# this will be accessible to this route and all beneath it but
# won't be visible to the parent route's handlers
this.otherInfo = 'hello';
},
leave: function () {
this.view.hideSummary();
}
});
base = new router.Route()
meetingroute = base.extend('/meeting/:id', {
enter: function (id) {
var _this = this;
# return a promise as this isn't finished until
# the meeting view is loaded. Actions after this
# are queued until it's done
return loadMeetingView(id).done(function (view) {
_this.view = view;
});
},
leave: function () {
this.view.destroy();
}
});
{"href": "http://example.com/resource"} convention for links.
http://almostobsolete.net/talks/londonjs/
Thomas Parslow
tom@almostobsolete.net
@almostobsolete
http://almostobsolete.net