Ember Data: Adapters, Serializers & Persisting Changes
Adapters: How to Talk to the API
// Defines URL format, HTTP methods, auth headers -- letting the
// Store stay agnostic to a specific backend's exact conventions
import JSONAPIAdapter from '@ember-data/adapter/json-api';
export default class ApplicationAdapter extends JSONAPIAdapter {
host = 'https://api.example.com';
get headers() {
return { Authorization: `Bearer ${this.session.token}` };
}
}Serializers: Translating Data Shape
// Converts backend snake_case field names to Ember's camelCase --
// {"first_name": "Alice"} <-> firstName
import JSONAPISerializer from '@ember-data/serializer/json-api';
import { underscore } from '@ember/string';
export default class ApplicationSerializer extends JSONAPISerializer {
keyForAttribute(attr) {
return underscore(attr);
}
}Creating & Updating Records
// createRecord() creates a LOCAL-ONLY record -- no network request
// until .save() is explicitly called
const newUser = this.store.createRecord('user', {
email: 'alice@example.com',
});
await newUser.save(); // POST request sent here
// Updating an existing record: mutate the tracked attribute,
// then call save() -- Ember Data sends the appropriate PATCH/PUT
user.email = 'newemail@example.com';
await user.save();
// rollbackAttributes() discards unsaved local changes -- useful
// for a form's "Cancel" action
user.rollbackAttributes();Handling Validation Errors
{{! A rejected save() surfaces field-level backend validation
errors -- e.g. "Email has already been taken" }}
{{#if this.user.errors.email}}
<span class="error">{{this.user.errors.email.firstObject.message}}</span>
{{/if}}The Full Request Flow
store.findRecord('user', 5) -- checks cache first; if missing, delegates to the Adapter to fetch (deciding HOW); the response passes through the Serializer to translate format (deciding the SHAPE); the result instantiates/updates a Model, which the Store caches and returns.
Keep your own version of these notes — editable, searchable, and organised by your stack.
Start free