Implementing non-RESTful actions with Ember Data
In my recent post I mentioned some strategies of handling non-strictly CRUD / RESTful actions in API. One of them was adding extra actions beyond creating, updating and deleting resources. As it's not a standard solution and some data layers on client side (like Ember Data) don't handle it out-of-box, I was asked by some developers what's the best way to handle such actions. So let's see how can we hack into Ember Data and make it smooth.
Our use case: publishing articles
Let's reuse the example from the previous blog post - the articles and publishing process. What we are going to do is to add publish
function to our Article
model, which simply sends PATCH
request to API endpoint with /api/articles/:id/publish
URL.
First thought about implementation could be using Ember.$.ajax
call with manually passing URL and all the data. But that's not really a great solution. For simple cases it may work, but what about sending some custom headers, using namespaces and other options? For these reasons we should encapsulate all the logic within adapter - the layer that's responsible for communication with the API. So let's add publish
function to our article adapter for executing the request and use this function from article
model.
// app/adapters/article.js
import ApplicationAdapter from './application';
export default ApplicationAdapter.extend({
publish(id) {
return this.ajax(this.urlForPublishAction(id), 'PUT');
},
urlForPublishAction(id) {
return `${this.buildURL('article', id)}/publish`;
}
});
There are some interesting things going on here: the first one is that we use ajax
function defined in adapter, which is also utilized when peforming all other requests in Ember Data. It takes care of setting up options like headers, proper success and error handling etc., so we should always use this function instead of simple Ember.$.ajax
. Another thing is buildURL
function, which builds, well, URL for given resource represented by model name (with given id if present) considering adapter options like host
or namespace
. By using these functions we ensure the consistency between all API calls.
To make it work we just need to find proper adapter for article
model and call publish
function on this adapter from the model:
// app/models/article.js
import Ember from 'ember';
import DS from 'ember-data';
export default DS.Model.extend({
publish() {
let modelName = this.constructor.modelName;
let adapter = this.store.adapterFor(modelName);
return adapter.publish(this.get('id'));
}
});
To avoid hardcoding model name we take it from the constructor
, then use it as a name of adapter we want to fetch from owner
(or container
in Ember versions prior to 2.3) and finally call the proper method on adapter with id as argument.
What if we needed to pass the serialized attributes as well? The third argument of ajax
function in adapter is a hash, so we would need to pass the serialized article as data
param there. Here's a quick example, starting from the model:
// app/models/article.js
import Ember from 'ember';
import DS from 'ember-data';
export default DS.Model.extend({
publish() {
let modelName = this.constructor.modelName;
let adapter = this.store.adapterFor(modelName);
return adapter.publish(this.get('id'), this.serialize());
}
});
and here's the adapter:
// app/adapters/article.js
import ApplicationAdapter from './application';
export default ApplicationAdapter.extend({
publish(id, serializedData) {
return this.ajax(this.urlForPublishAction(id), 'PUT', { data: serializedData });
},
urlForPublishAction(id) {
return `${this.buildURL('article', id)}/publish`;
}
});
And that's it!
Wrapping up
Non-RESTful actions are not supported out of the box by Ember Data, but they are not that hard to implement when using adapters.