DS.RESTAdapter

The REST adapter allows your store to communicate with an HTTP server by transmitting JSON via XHR. Most Ember.js apps that consume a JSON API should use the REST adapter.

This adapter is designed around the idea that the JSON exchanged with the server should be conventional.

Success and failure

The REST adapter will consider a success any response with a status code of the 2xx family ("Success"), as well as 304 ("Not Modified"). Any other status code will be considered a failure.

On success, the request promise will be resolved with the full response payload.

Failed responses with status code 422 ("Unprocessable Entity") will be considered "invalid". The response will be discarded, except for the errors key. The request promise will be rejected with a DS.InvalidError. This error object will encapsulate the saved errors value.

Any other status codes will be treated as an "adapter error". The request promise will be rejected, similarly to the "invalid" case, but with an instance of DS.AdapterError instead.

JSON Structure

The REST adapter expects the JSON returned from your server to follow these conventions.

Object Root

The JSON payload should be an object that contains the record inside a root property. For example, in response to a GET request for /posts/1, the JSON should look like this:

1
2
3
4
5
6
7
{
  "posts": {
    "id": 1,
    "title": "I'm Running to Reform the W3C's Tag",
    "author": "Yehuda Katz"
  }
}

Similarly, in response to a GET request for /posts, the JSON should look like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
  "posts": [
    {
      "id": 1,
      "title": "I'm Running to Reform the W3C's Tag",
      "author": "Yehuda Katz"
    },
    {
      "id": 2,
      "title": "Rails is omakase",
      "author": "D2H"
    }
  ]
}

Note that the object root can be pluralized for both a single-object response and an array response: the REST adapter is not strict on this. Further, if the HTTP server responds to a GET request to /posts/1 (e.g. the response to a findRecord query) with more than one object in the array, Ember Data will only display the object with the matching ID.

Conventional Names

Attribute names in your JSON payload should be the camelCased versions of the attributes in your Ember.js models.

For example, if you have a Person model:

1
2
3
4
5
6
7
import DS from 'ember-data';

export default DS.Model.extend({
  firstName: DS.attr('string'),
  lastName: DS.attr('string'),
  occupation: DS.attr('string')
});

The JSON returned should look like this:

1
2
3
4
5
6
7
8
{
  "people": {
    "id": 5,
    "firstName": "Zaphod",
    "lastName": "Beeblebrox",
    "occupation": "President"
  }
}

Relationships

Relationships are usually represented by ids to the record in the relationship. The related records can then be sideloaded in the response under a key for the type.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
  "posts": {
    "id": 5,
    "title": "I'm Running to Reform the W3C's Tag",
    "author": "Yehuda Katz",
    "comments": [1, 2]
  },
  "comments": [{
    "id": 1,
    "author": "User 1",
    "message": "First!",
  }, {
    "id": 2,
    "author": "User 2",
    "message": "Good Luck!",
  }]
}

If the records in the relationship are not known when the response is serialized its also possible to represent the relationship as a url using the links key in the response. Ember Data will fetch this url to resolve the relationship when it is accessed for the first time.

1
2
3
4
5
6
7
8
9
10
{
  "posts": {
    "id": 5,
    "title": "I'm Running to Reform the W3C's Tag",
    "author": "Yehuda Katz",
    "links": {
      "comments": "/posts/5/comments"
    }
  }
}

Errors

If a response is considered a failure, the JSON payload is expected to include a top-level key errors, detailing any specific issues. For example:

1
2
3
4
5
{
  "errors": {
    "msg": "Something went wrong"
  }
}

This adapter does not make any assumptions as to the format of the errors object. It will simply be passed along as is, wrapped in an instance of DS.InvalidError or DS.AdapterError. The serializer can interpret it afterwards.

Customization

Endpoint path customization

Endpoint paths can be prefixed with a namespace by setting the namespace property on the adapter:

1
2
3
4
5
import DS from 'ember-data';

export default DS.RESTAdapter.extend({
  namespace: 'api/1'
});

Requests for the Person model would now target /api/1/people/1.

Host customization

An adapter can target other hosts by setting the host property.

1
2
3
4
5
import DS from 'ember-data';

export default DS.RESTAdapter.extend({
  host: 'https://api.example.com'
});

Headers customization

Some APIs require HTTP headers, e.g. to provide an API key. Arbitrary headers can be set as key/value pairs on the RESTAdapter's headers object and Ember Data will send them along with each ajax request.

1
2
3
4
5
6
7
8
import DS from 'ember-data';

export default DS.RESTAdapter.extend({
  headers: {
    'API_KEY': 'secret key',
    'ANOTHER_HEADER': 'Some header value'
  }
});

headers can also be used as a computed property to support dynamic headers. In the example below, the session object has been injected into an adapter by Ember's container.

1
2
3
4
5
6
7
8
9
10
import DS from 'ember-data';

export default DS.RESTAdapter.extend({
  headers: Ember.computed('session.authToken', function() {
    return {
      'API_KEY': this.get('session.authToken'),
      'ANOTHER_HEADER': 'Some header value'
    };
  })
});

In some cases, your dynamic headers may require data from some object outside of Ember's observer system (for example document.cookie). You can use the volatile function to set the property into a non-cached mode causing the headers to be recomputed with every request.

1
2
3
4
5
6
7
8
9
10
import DS from 'ember-data';

export default DS.RESTAdapter.extend({
  headers: Ember.computed(function() {
    return {
      'API_KEY': Ember.get(document.cookie.match(/apiKey\=([^;]*)/), '1'),
      'ANOTHER_HEADER': 'Some header value'
    };
  }).volatile()
});

Show:

coalesceFindRequests

By default the RESTAdapter will send each find request coming from a store.find or from accessing a relationship separately to the server. If your server supports passing ids as a query string, you can set coalesceFindRequests to true to coalesce all find requests within a single runloop.

For example, if you have an initial payload of:

1
2
3
4
5
6
{
  post: {
    id: 1,
    comments: [1, 2]
  }
}

By default calling post.get('comments') will trigger the following requests(assuming the comments haven't been loaded before):

1
2
GET /comments/1
GET /comments/2

If you set coalesceFindRequests to true it will instead trigger the following request:

1
GET /comments?ids[]=1&ids[]=2

Setting coalesceFindRequests to true also works for store.find requests and belongsTo relationships accessed within the same runloop. If you set coalesceFindRequests: true

1
2
store.findRecord('comment', 1);
store.findRecord('comment', 2);

will also send a request to: GET /comments?ids[]=1&ids[]=2

Note: Requests coalescing rely on URL building strategy. So if you override buildURL in your app groupRecordsForFindMany more likely should be overridden as well in order for coalescing to work.

coalesceFindRequests

By default the store will try to coalesce all fetchRecord calls within the same runloop into as few requests as possible by calling groupRecordsForFindMany and passing it into a findMany call. You can opt out of this behaviour by either not implementing the findMany hook or by setting coalesceFindRequests to false.

defaultSerializer

If you would like your adapter to use a custom serializer you can set the defaultSerializer property to be the name of the custom serializer.

Note the defaultSerializer serializer has a lower priority than a model specific serializer (i.e. PostSerializer) or the application serializer.

1
2
3
4
5
import DS from 'ember-data';

export default DS.Adapter.extend({
  defaultSerializer: 'django'
});

headers

Some APIs require HTTP headers, e.g. to provide an API key. Arbitrary headers can be set as key/value pairs on the RESTAdapter's headers object and Ember Data will send them along with each ajax request. For dynamic headers see headers customization.

1
2
3
4
5
6
7
8
import DS from 'ember-data';

export default DS.RESTAdapter.extend({
  headers: {
    'API_KEY': 'secret key',
    'ANOTHER_HEADER': 'Some header value'
  }
});

host

An adapter can target other hosts by setting the host property.

1
2
3
4
5
import DS from 'ember-data';

export default DS.RESTAdapter.extend({
  host: 'https://api.example.com'
});

Requests for the Post model would now target https://api.example.com/post/.

namespace

Endpoint paths can be prefixed with a namespace by setting the namespace property on the adapter:

1
2
3
4
5
import DS from 'ember-data';

export default DS.RESTAdapter.extend({
  namespace: 'api/1'
});

Requests for the Post model would now target /api/1/post/.