Non-standard REST Actions in Ember Data

In our previous post we loaded a subset of models from a non-standard API. As the store was populated with the response, customizing query was appropriate.

But not every endpoint loads or saves models. We may have unRESTful, less-than-ideal API operations such as:

  1. a PATCH /tasks/1/suspend call that returns a boolean
  2. a GET /task/suspend?id=1 call that returns an HTTP 204 no matter what
  3. a POST /completeAllTasks call that returns an integer with the amount of tasks marked as completed

Can we still use Ember Data or are we out of luck?

Example action

Let’s use the first example: the suspend action must end up calling PATCH /tasks/:id/suspend. This is how we would like to call the action from our route:

export default Ember.Route.extend({

  actions: {
    suspend() {
      this.currentModel.suspend();
    }
  }

  // ...

});

Approach #1: ember-api-actions

With the help of the ember-api-actions add-on, our model could look like this:

// app/models/task.js

import { memberAction } from 'ember-api-actions';

export default Model.extend({

  name: attr(),
  description: attr(),
  owner: belongsTo(),
  tasks: hasMany('task'),

  suspend: memberAction({ path: 'suspend', type: 'patch' })

});

That’s it!

Adding parameters

Need to pass parameters, like max: 9? Simple as cake:

suspend() {
  this.currentModel.suspend({ max: 9 }).then(response => {
    // do something with the response
  });
}

These params will be passed on to the ajax call.

ember-api-actions also allows to act on collections. See an example here.

If for some reason we don’t want to use this add-on, or we need to further fine-tune…

Approach #2: Adapter time

We might be inclined to think “Non-standard API? I need to make an ajax call with Ember.$.ajax”, but that’s not necessarily the best idea. We miss out on configuration parameters already available to the adapter: namespace, host, custom headers and so on.

So how do we tweak the adapter (and the model) for such a custom call?

For starters, our model will delegate the action to its adapter:

// app/models/task.js

export default Model.extend({

  name: attr(),
  description: attr(),
  owner: belongsTo(),
  tasks: hasMany('task'),

  suspend() {
    const adapter = this.store.adapterFor(this.constructor.modelName);
    return adapter.suspend(this);
  }

});

Our adapter can now take advantage of adapter helpers such as buildURL and ajax!

// app/adapters/task.js

export default ApplicationAdapter.extend({

  suspend(model) {
    const url = this.buildURL('task', model.get('id')) + "/suspend";
    return this.ajax(url, 'PATCH');
  }

});

Again, super simple!

Adding parameters (and query parameters!)

Let’s suppose we now need to hit the POST /task/suspend?id=1 endpoint, supplying the body parameter max=9. We want to invoke it just like with the add-on:

suspend() {
  this.currentModel.suspend({ max: 9 }).then(response => {
    // do something with the response
  });
}

Our suspend function will have to be modified to accept parameters:

// app/models/task.js

export default Model.extend({

  name: attr(),
  description: attr(),
  owner: belongsTo(),
  tasks: hasMany('task'),

  suspend(params) {
    const adapter = this.store.adapterFor(this.constructor.modelName);
    return adapter.suspend(this, params);
  }

});

And the adapter, in turn, could look like this:

// app/adapters/task.js

export default ApplicationAdapter.extend({

  suspend(model, params) {
    const id = model.get('id');
    const url = `${this.host}/${this.namespace}/task/suspend?id=${id}`
    return this.ajax(url, 'POST', { data: params });
  }

});

It does work!

This was a short introduction to customizing non-standard endpoints. Let me know what yours look like and I’ll try to include them here!

Remember, if you are receiving a model (or collection of models) you could place the response into pushPayload. But I definitely recommend customizing findRecord, query, etc. as discussed in this previous post.

Enjoyed this article? Don't miss my next one!

Leave me your e-mail for content that will help you master Ember:

Do you want to master Ember fast?

Leave me your e-mail for helpful updates delivered straight to your inbox.

(A few e-mails per month. No BS. Unsubscribe anytime!)