Ember Data — The Data Layer for Your Application
In this tutorial, you will learn about Ember Data. We cover key concepts, practical examples, and best practices to help you master this topic.
Ember Data is the official data persistence library for Ember.js. It manages model instances, relationships, Caching, and server synchronization. The store is the central Repository that coordinates finding, creating, updating, and deleting records.
What You'll Learn
You will learn how to use Ember Data, define models with attributes and relationships, use the store for CRUD operations, query records, and handle caching.
Why It Matters
Ember Data eliminates AJAX boilerplate. Instead of writing fetch calls and manually updating state, you call store.findAll('post') and Ember Data handles caching, deduplication, and background updates.
Real-World Use
An e-commerce platform uses Ember Data to manage products, orders, customers, and inventory. When a customer places an order, store.createRecord('order') creates a new record, relationships to products are established automatically, and the inventory model is updated through the store.
flowchart LR
A[Route] --> B[store.findAll]
A --> C[store.findRecord]
A --> D[store.query]
B --> E[Adapter]
C --> E
D --> E
E --> F[Server API]
F --> G[Serializer]
G --> H[Model instances]
H --> I[Template]
Setting Up Ember Data
Ember Data ships with new Ember projects. Verify it is installed:
npm list @ember-data/model @ember-data/store @ember-data/adapter @ember-data/serializer
Defining Models
Models define the schema of your data.
// app/models/user.js
import Model, { attr, hasMany } from '@ember-data/model';
export default class UserModel extends Model {
@attr('string') name;
@attr('string') email;
@attr('string') role;
@attr('date') createdAt;
@attr('boolean') isActive;
@hasMany('post') posts;
get displayName() {
return this.name || this.email;
}
get isAdmin() {
return this.role === 'admin';
}
}
// app/models/post.js
import Model, { attr, belongsTo, hasMany } from '@ember-data/model';
export default class PostModel extends Model {
@attr('string') title;
@attr('string') body;
@attr('boolean') published;
@attr('date') createdAt;
@belongsTo('user') author;
@hasMany('comment') comments;
get excerpt() {
return this.body?.substring(0, 200);
}
get wordCount() {
return this.body?.split(' ').length || 0;
}
}
Using the Store
Inject the store into routes, controllers, or components.
// app/routes/posts.js
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
export default class PostsRoute extends Route {
@service store;
async model() {
return this.store.findAll('post');
}
}
CRUD Operations with the Store
// app/routes/admin/posts.js
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
export default class AdminPostsRoute extends Route {
@service store;
@service router;
async model() {
return this.store.findAll('post');
}
@action
async createPost(title, body) {
let post = this.store.createRecord('post', {
title,
body,
published: false
});
try {
await post.save();
console.log('Post created:', post.id);
this.router.transitionTo('admin.posts');
} catch (error) {
console.error('Failed to create post:', error);
post.rollbackAttributes();
}
}
@action
async publishPost(post) {
post.published = true;
try {
await post.save();
} catch (error) {
post.rollbackAttributes();
}
}
@action
async deletePost(post) {
try {
await post.destroyRecord();
} catch (error) {
console.error('Failed to delete:', error);
}
}
}
Querying Records
// Find all records
let posts = await this.store.findAll('post');
// Find by ID
let post = await this.store.findRecord('post', 123);
// Query with parameters
let results = await this.store.query('post', {
filter: { category: 'security' },
sort: '-createdAt',
page: { limit: 10 }
});
// Query by a single attribute
let userPosts = await this.store.query('post', {
filter: { author: userId }
});
// Peek at cached records without fetching
let cached = this.store.peekAll('post');
let cachedRecord = this.store.peekRecord('post', 123);
Relationships
Access related records through relationship attributes.
// app/routes/users/user.js
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
export default class UsersUserRoute extends Route {
@service store;
async model(params) {
return this.store.findRecord('user', params.user_id, {
include: 'posts,comments'
});
}
async afterModel(user) {
// Access related records
let posts = await user.posts;
console.log(`${user.name} has ${posts.length} posts`);
}
}
Loading and Error States
Ember Data models have states: loading, loaded, error, deleted.
// Check record state
let post = this.store.peekRecord('post', 123);
if (post.isLoading) {
console.log('Still loading...');
} else if (post.isError) {
console.log('Load failed');
} else if (post.isDeleted) {
console.log('Record was deleted');
} else if (post.isNew) {
console.log('Not saved yet');
}
// Reload a record
await post.reload();
// Check if record has changes
if (post.hasDirtyAttributes) {
console.log('Unsaved changes:', post.changedAttributes());
}
Custom Queries with Adapter
For complex server endpoints, use store.query() or custom adapter methods.
// app/adapters/post.js
import JSONAPIAdapter from '@ember-data/adapter/json-api';
export default class PostAdapter extends JSONAPIAdapter {
urlForFindAll(modelName) {
return '/api/v2/posts';
}
urlForQuery(query, modelName) {
let url = '/api/v2/posts';
if (query.category) {
url += `/category/${query.category}`;
}
return url;
}
}
Testing with Ember Data
// tests/acceptance/posts-test.js
import { module, test } from 'qunit';
import { setupApplicationTest } from 'ember-qunit';
import { currentURL, visit } from '@ember/test-helpers';
import { setupMirage } from 'ember-cli-mirage';
module('Acceptance | posts', function(hooks) {
setupApplicationTest(hooks);
setupMirage(hooks);
test('user can view posts', async function(assert) {
this.server.createList('post', 3);
await visit('/posts');
assert.dom('[data-test-post]').exists({ count: 3 });
});
test('user can create a post', async function(assert) {
await visit('/admin/posts/new');
await fillIn('[data-test-title]', 'New Post');
await click('[data-test-submit]');
assert.dom('[data-test-success]').exists();
});
});
Common Mistakes
- Not handling save errors.
save()throws on server errors. Always wrap in try-catch and handle validation errors from the response. - Mutating belongsTo/hasMany directly. Set relationships via
set()or creating records with the relationship. Push only works with therecordsarray. - Forgetting
rollbackAttributes()after failed saves. Dirty attributes stay dirty. Rollback on error to reset state. - Using
peekAllexpecting fresh data. peekAll returns cached data. UsefindAllfor fresh data or callreload()on cached results. - Not using
includefor sideloaded relationships. N+1 queries happen when you access relationship attributes without including them in the request.
Practice Questions
- How do you create a new record with Ember Data?
- What is the difference between
findAllandpeekAll? - How do you define a one-to-many relationship?
- What does
rollbackAttributes()do? - Challenge: Set up an Ember Data schema with three related models:
Category(hasMany products),Product(belongsTo category, hasMany reviews),Review(belongsTo product, has: rating, text). Write a route that loads a category, its products, and each product's average rating. Display the data in nested templates.
FAQ
Mini Project
Create a complete Ember Data setup for a blog: models for Post, Author, Comment, and Tag. Set up relationships. Create a route that lists all posts with their authors and comment counts. Create a detail route that shows a post, its author, and comments. Add a form to create new comments. Use Mirage for testing.
What's Next
Now that you understand Ember Data, learn Ember Models for attribute types and transforms. Then explore Ember Adapters for API configuration.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro