Non-standard REST Actions in Ember Data

Last reviewed in June 2019 with Ember Octane

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 non-RESTful 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: suspend must end up calling PATCH /tasks/:id/suspend. This is how we would like to call it:

export default class SomeController extends Controller {
  suspend() {
    this.model.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 DS from 'ember-data';
import { memberAction } from 'ember-api-actions';

const { Model, attr } = DS;

export default class TaskModel extends Model {

  attr() name;
  attr() description;
  belongsTo() owner;
  hasMany() tasks;

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

});

That's it!

Adding parameters

Need to pass parameters, like max: 9? Piece of cake:

export default class SomeController extends Controller {
  suspend() {
    this.model.suspend({ max: 9 });
  }
}

These params will be passed onto the XHR 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", 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 function to its adapter:

// app/models/task.js

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

const { Model, attr } = DS;

export default class TaskModel extends Model {

  attr() name;
  attr() description;
  belongsTo() owner;
  hasMany() tasks;

  suspend() {
    const adapter = this.store.adapterFor('task');
    return adapter.suspend(this);
  }

}

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

// app/adapters/task.js

import ApplicationAdapter from './application';

export default class TaskAdapter extends ApplicationAdapter {

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

}

Again, super simple!

Octane news & best practices, straight to your inbox?

Snacks is the best of Ember Octane in a highly digestible monthly newsletter. (No spam. EVER.)

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:

export default class SomeController extends Controller {
  suspend() {
    this.model.suspend({ max: 9 });
  }
}

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

// app/models/task.js

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

const { Model, attr } = DS;

export default class TaskModel extends Model {

  attr() name;
  attr() description;
  belongsTo() owner;
  hasMany() tasks;

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

}

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

// app/adapters/task.js

import ApplicationAdapter from './application';

export default class TaskAdapter extends ApplicationAdapter {

  suspend(model, params) {
    const url = `${this.buildURL('task')}/suspend?id=${model.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? Join Snacks!

Snacks is the best of Ember Octane in a highly digestible monthly newsletter. (No spam. EVER.)