Ember.js is a productive, battle-tested JavaScript framework for building modern web applications. This guide will take you through the fundamentals of Ember, from installation to building your first components.
What is Ember.js?
Ember.js is an opinionated framework that provides strong conventions, eliminating the need to make endless choices about architecture. It includes everything you need to build ambitious web applications:
- Templating engine (Handlebars)
- Router for URL management
- Data layer (Ember Data)
- Testing tools built-in
- CLI for project scaffolding
Installation and Setup
Prerequisites
Before installing Ember, ensure you have Node.js (14.x or higher) and npm installed.
# Install Ember CLI globally
npm install -g ember-cli
# Create a new Ember application
ember new my-ember-app
# Navigate to your project
cd my-ember-app
# Start the development server
ember serveProject Structure
my-ember-app/
├── app/
│ ├── components/ # Reusable UI components
│ ├── controllers/ # Business logic
│ ├── models/ # Data models
│ ├── routes/ # Route handlers
│ ├── styles/ # Global styles
│ ├── templates/ # Handlebars templates
│ ├── app.js # Application entry point
│ ├── index.html # Main HTML file
│ └── router.js # Route configuration
├── config/
│ └── environment.js # Environment settings
├── public/ # Static assets
├── tests/ # Test files
├── ember-cli-build.js # Build configuration
└── package.jsonCore Concepts
Routes
Routes are the heart of an Ember application. They define the URL structure and load the necessary data.
// app/routes/index.js
import Route from '@ember/routing/route';
export default class IndexRoute extends Route {
model() {
// This data is automatically available to the template
return ['Item 1', 'Item 2', 'Item 3'];
}
}Templates
Ember uses Handlebars templates for the UI:
{{! app/templates/index.hbs }}
<div class="container">
<h1>Welcome to Ember!</h1>
<ul>
{{#each @model as |item|}}
<li>{{item}}</li>
{{/each}}
</ul>
</div>Components
Components are reusable UI elements:
// app/components/user-card.js
import Component from '@glimmer/component';
export default class UserCardComponent extends Component {
get fullName() {
return `${this.args.firstName} ${this.args.lastName}`;
}
}{{! app/components/user-card.hbs }}
<div class="user-card">
<h2>{{this.fullName}}</h2>
<p>{{this.args.email}}</p>
</div>Usage:
<UserCard
@firstName="John"
@lastName="Doe"
@email="john@example.com"
/>Controllers
Controllers handle user interactions and temporary state:
// app/controllers/index.js
import Controller from '@ember/controller';
import { tracked } from '@glimmer/tracking';
export default class IndexController extends Controller {
@tracked searchTerm = '';
get filteredItems() {
if (!this.searchTerm) return this.model;
return this.model.filter(item =>
item.toLowerCase().includes(this.searchTerm.toLowerCase())
);
}
actions = {
updateSearchTerm(event) {
this.searchTerm = event.target.value;
}
}
}Models and Ember Data
Ember Data provides a standardized way to communicate with APIs:
// app/models/user.js
import Model, { attr } from '@ember-data/model';
export default class UserModel extends Model {
@attr('string') name;
@attr('string') email;
@attr('date', { defaultValue: () => new Date() }) createdAt;
}Adapters
Adapters define how your app communicates with the backend:
// app/adapters/application.js
import RESTAdapter from '@ember-data/adapter/rest';
export default class ApplicationAdapter extends RESTAdapter {
namespace = 'api';
host = 'https://api.example.com';
}Serializers
Serializers format the data:
// app/serializers/user.js
import RESTSerializer from '@ember-data/serializer/rest';
export default class UserSerializer extends RESTSerializer {
normalizeResponse(store, primaryModelClass, payload, id, requestType) {
payload = { user: payload };
return super.normalizeResponse(store, primaryModelClass, payload, id, requestType);
}
}Services
Services provide long-lived objects that can be shared across routes and components:
// app/services/shopping-cart.js
import Service from '@ember/service';
import { tracked } from '@glimmer/tracking';
export default class ShoppingCartService extends Service {
@tracked items = [];
addItem(item) {
this.items = [...this.items, item];
}
removeItem(itemId) {
this.items = this.items.filter(i => i.id !== itemId);
}
get total() {
return this.items.reduce((sum, item) => sum + item.price, 0);
}
}Using the service:
import { inject as service } from '@ember/service';
export default class CartComponent extends Component {
@service shoppingCart;
get itemCount() {
return this.shoppingCart.items.length;
}
}Helpers
Helpers are functions that can be used in templates:
// app/helpers/format-date.js
import { helper } from '@ember/component/helper';
export function formatDate([date]) {
return new Intl.DateTimeFormat('en-US').format(new Date(date));
}
export default helper(formatDate);Usage:
{{format-date this.user.createdAt}}Ember CLI Commands
# Generate a new route
ember generate route about
# Generate a component
ember generate component user-profile
# Generate a service
ember generate service session
# Build for production
ember build --environment=production
# Run tests
ember test
# Serve with specific port
ember serve --port 4201Testing
Ember includes testing tools out of the box:
// tests/integration/components/user-card-test.js
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile';
module('Integration | Component | user-card', function(hooks) {
setupRenderingTest(hooks);
test('it renders user information', async function(assert) {
this.setProperties({
firstName: 'John',
lastName: 'Doe',
email: 'john@example.com'
});
await render(hbs`<UserCard
@firstName={{this.firstName}}
@lastName={{this.lastName}}
@email={{this.email}}
/>`);
assert.dom('h2').hasText('John Doe');
assert.dom('p').hasText('john@example.com');
});
});Development Best Practices
- 1Follow Naming Conventions: Ember is opinionated about naming. Use dashes for component names and camelCase for JavaScript files.
- 1Use Tracked Properties: Always use
@trackedfor reactive properties in Octane.
- 1Keep Components Small: Components should be focused and reusable.
- 1Leverage Ember Data: Use Ember Data for consistent data management.
- 1Write Tests: Ember makes testing easy—take advantage of it.
- 1Use the CLI: The Ember CLI is powerful—use it for generating code.
Conclusion
Ember.js provides a complete solution for building ambitious web applications. Its strong conventions mean you spend less time making decisions and more time building features. With built-in routing, data management, and testing, Ember gives you everything you need to create maintainable, scalable applications.
Start with the basics: understand routes, templates, and components. Then explore Ember Data for API integration and services for shared state. The learning curve might be steeper than some frameworks, but the productivity gains are worth it.