Skip to content

Ember Data — The Data Layer for Your Application

DodaTech Updated 2026-06-28 6 min read

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

  1. Not handling save errors. save() throws on server errors. Always wrap in try-catch and handle validation errors from the response.
  2. Mutating belongsTo/hasMany directly. Set relationships via set() or creating records with the relationship. Push only works with the records array.
  3. Forgetting rollbackAttributes() after failed saves. Dirty attributes stay dirty. Rollback on error to reset state.
  4. Using peekAll expecting fresh data. peekAll returns cached data. Use findAll for fresh data or call reload() on cached results.
  5. Not using include for sideloaded relationships. N+1 queries happen when you access relationship attributes without including them in the request.

Practice Questions

  1. How do you create a new record with Ember Data?
  2. What is the difference between findAll and peekAll?
  3. How do you define a one-to-many relationship?
  4. What does rollbackAttributes() do?
  5. 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

Is Ember Data required to use Ember?

No, but it is the standard approach. You can use plain fetch or other libraries.

Does Ember Data work with any backend?

Yes. Use an adapter for JSON:API, REST, or custom formats.

How does Ember Data handle caching?

Records are cached in the store. findAll returns cached data if available and updates in background.

Can I use Ember Data without a server?

Yes. Use ember-cli-mirage for mocking or create an adapter that reads from localStorage.

How do I handle optimistic updates?

Update the record locally before calling save(). Rollback if the server rejects.

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