Skip to content

Ember Project — Build a Complete Blog Application

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you will learn about Ember Project. We cover key concepts, practical examples, and best practices to help you master this topic.

Build a complete blog application using Ember.js. This project combines routes, components, Ember Data models, services, authentication, and testing into a working application. You will create posts, manage authors, handle comments, and deploy the final product.

What You'll Learn

You will integrate every Ember concept into a single application. By the end, you will have a working blog that demonstrates production-ready Ember architecture.

Why It Matters

Building a complete application solidifies every concept you have learned. You will see how routes connect to templates, how services manage shared state, and how all pieces work together in a real deployment.

Real-World Use

This blog application demonstrates patterns used in content management systems, documentation sites, and news platforms. The architecture scales from small blogs to enterprise content platforms with thousands of posts.

flowchart TD
    A[Router] --> B[Posts Route]
    A --> C[Post Route]
    A --> D[Auth Route]
    B --> E[PostList Component]
    C --> F[PostDetail Component]
    C --> G[CommentList Component]
    D --> H[LoginForm Component]
    I[Session Service] --> A
    I --> C
    J[Ember Data Store] --> B
    J --> C

Project Setup

ember new ember-blog --typescript
cd ember-blog
ember install @ember-data/model
ember install ember-cli-mirage
ember install ember-auto-import

Step 1: Define Models

// app/models/post.js
import Model, { attr, belongsTo, hasMany } from '@ember-data/model';

export default class PostModel extends Model {
  @attr('string') title;
  @attr('string') slug;
  @attr('string') excerpt;
  @attr('string') body;
  @attr('boolean') published;
  @attr('date') createdAt;
  @attr('date') updatedAt;

  @belongsTo('user') author;
  @hasMany('comment') comments;

  get publishedDate() {
    return this.createdAt?.toLocaleDateString('en-US', {
      year: 'numeric', month: 'long', day: 'numeric'
    });
  }

  get readingTime() {
    let words = this.body?.split(/\s+/).length || 0;
    return Math.ceil(words / 200);
  }
}

// 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') avatar;
  @attr('string') bio;
  @hasMany('post') posts;
}

// app/models/comment.js
import Model, { attr, belongsTo } from '@ember-data/model';

export default class CommentModel extends Model {
  @attr('string') author;
  @attr('string') body;
  @attr('date') createdAt;
  @belongsTo('post') post;
}

Step 2: Define Routes

// app/router.js
Router.map(function() {
  this.route('posts', { path: '/' }, function() {
    this.route('post', { path: ':slug' });
  });
  this.route('login');
  this.route('admin', function() {
    this.route('posts');
    this.route('post', { path: 'post/new' });
    this.route('post', { path: 'post/:id/edit' });
  });
});

Step 3: Create Route Handlers

// app/routes/posts/index.js
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';

export default class PostsIndexRoute extends Route {
  @service store;

  async model() {
    return this.store.findAll('post', { filter: { published: true } });
  }
}

// app/routes/posts/post.js
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';

export default class PostsPostRoute extends Route {
  @service store;

  async model(params) {
    return this.store.query('post', { filter: { slug: params.slug } })
      .then(posts => posts.firstObject);
  }
}

Step 4: Build Templates

{{! app/templates/posts/index.hbs }}
<div class="posts-page">
  <h1>Blog</h1>

  <div class="posts-grid">
    {{#each this.model as |post|}}
      <article class="post-card">
        <h2>
          <LinkTo @route="posts.post" @model={{post.slug}}>
            {{post.title}}
          </LinkTo>
        </h2>
        <p class="post-meta">
          By {{post.author.name}} on {{post.publishedDate}}
          | {{post.readingTime}} min read
        </p>
        <p class="post-excerpt">{{post.excerpt}}</p>
      </article>
    {{else}}
      <p class="empty">No posts yet. Check back soon!</p>
    {{/each}}
  </div>
</div>
{{! app/templates/posts/post.hbs }}
<article class="post-detail">
  <header>
    <h1>{{this.model.title}}</h1>
    <div class="post-meta">
      <img src={{this.model.author.avatar}} alt="" class="avatar" />
      <span>{{this.model.author.name}}</span>
      <span>{{this.model.publishedDate}}</span>
      <span>{{this.model.readingTime}} min read</span>
    </div>
  </header>

  <div class="post-body">
    {{this.model.body}}
  </div>

  <section class="comments">
    <h2>Comments</h2>
    {{#each this.model.comments as |comment|}}
      <div class="comment">
        <strong>{{comment.author}}</strong>
        <p>{{comment.body}}</p>
        <small>{{comment.createdAt}}</small>
      </div>
    {{else}}
      <p>No comments yet.</p>
    {{/each}}
  </section>
</article>

Step 5: Create Services

// app/services/session.js
import Service from '@ember/service';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';

export default class SessionService extends Service {
  @tracked user = null;
  @tracked token = localStorage.getItem('blog-token');

  get isAuthenticated() {
    return !!this.user;
  }

  @action
  async login(email, password) {
    let response = await fetch('/api/auth/login', {
      method: 'POST',
      body: JSON.stringify({ email, password })
    });

    if (!response.ok) throw new Error('Login failed');

    let data = await response.json();
    this.user = data.user;
    this.token = data.token;
    localStorage.setItem('blog-token', data.token);
    return data.user;
  }

  @action
  logout() {
    this.user = null;
    this.token = null;
    localStorage.removeItem('blog-token');
  }
}

Step 6: Admin Components

// app/components/post-form.js
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { inject as service } from '@ember/service';

export default class PostFormComponent extends Component {
  @service store;
  @service router;

  @tracked title = '';
  @tracked body = '';
  @tracked excerpt = '';
  @tracked published = false;
  @tracked errors = [];
  @tracked isSaving = false;

  constructor(owner, args) {
    super(owner, args);
    if (args.post) {
      this.title = args.post.title;
      this.body = args.post.body;
      this.excerpt = args.post.excerpt;
      this.published = args.post.published;
    }
  }

  get isFormValid() {
    return this.title.length > 0 && this.body.length > 0;
  }

  @action
  async save() {
    if (!this.isFormValid) {
      this.errors = ['Title and body are required'];
      return;
    }

    this.isSaving = true;
    this.errors = [];

    try {
      let post;
      if (this.args.post) {
        post = this.args.post;
        post.set({ title: this.title, body: this.body, excerpt: this.excerpt });
      } else {
        post = this.store.createRecord('post', {
          title: this.title,
          body: this.body,
          excerpt: this.excerpt,
          slug: this.title.toLowerCase().replace(/\s+/g, '-'),
          author: this.session.user
        });
      }

      await post.save();
      this.router.transitionTo('admin.posts');
    } catch (error) {
      this.errors = [error.message || 'Failed to save post'];
    } finally {
      this.isSaving = false;
    }
  }
}

Step 7: Testing with Mirage

// mirage/config.js
export default function() {
  this.namespace = 'api';

  this.get('/posts', (schema) => {
    return schema.posts.all();
  });

  this.get('/posts/:id', (schema, request) => {
    return schema.posts.find(request.params.id);
  });

  this.post('/posts', (schema, request) => {
    let attrs = JSON.parse(request.requestBody);
    return schema.posts.create(attrs);
  });
}

// mirage/factories/post.js
import { Factory } from 'ember-cli-mirage';
import faker from 'faker';

export default Factory.extend({
  title() { return faker.lorem.sentence(); },
  slug() { return faker.lorem.slug(); },
  body() { return faker.lorem.paragraphs(3); },
  published: true,
  createdAt() { return faker.date.past(); }
});

Deployment

ember build --prod
# Output in dist/ — deploy to any static host

Common Mistakes

  1. Not using the store for data management. Direct fetch calls bypass Ember Data's Caching and relationship management. Use the store consistently.
  2. Forgetting authentication checks in routes. Protected routes need a beforeModel hook that redirects to login if not authenticated.
  3. Not handling loading and error states. Templates without loading/error states show blank screens during slow requests.
  4. Overcomplicating the component hierarchy. Keep components focused. A post form component should not handle authentication logic.
  5. Not testing the critical paths. Test user login, post creation, post listing, and comment submission. These are the most important flows.

Practice Questions

  1. How does the session service manage authentication state?
  2. How does Ember Data handle relationships between posts, users, and comments?
  3. What is the purpose of the slug attribute in the Post model?
  4. How does the admin route protect against unauthorized access?
  5. Challenge: Add the following features to the blog: (1) Rich text editor for post body, (2) Image upload for post featured images, (3) RSS feed endpoint, (4) Social sharing links, (5) Related posts based on tags.

FAQ

How do I deploy an Ember app?

Run ember build --prod and deploy the dist/ directory to any static host.

Can I use this blog with a real API?

Yes. Replace Mirage with real adapters pointing to your backend.

How do I add categories and tags?

Create Category and Tag models with hasMany/belongsTo relationships to Post.

How do I implement search?

Use store.query('post', { q: searchTerm }) with server-side search.

How do I add pagination?

Add page and size query params. Use Ember Data's pagination metadata.

Mini Project

Extend the blog with: (1) User registration and profile pages, (2) Draft/publish workflow for posts, (3) Email notification service for new comments, (4) Admin dashboard with analytics (post views, comment counts), (5) Full-text search with highlighted results. Add acceptance tests for each new feature.

What's Next

Congratulations on completing Ember.js! Next, explore Aurelia for a different approach to modern web development. Or compare with Backbone.js for minimal frameworks.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro