Ember.js
Purpose
Build ambitious web applications using Ember's convention-over-configuration approach — Ember CLI for scaffolding, Ember Data for state management, and Octane-era patterns for modern components.
Agent Protocol
Trigger
Exact user phrases: "Ember setup", "Ember app", "Ember CLI", "Ember Data", "Ember Octane", "Ember component", "Ember route", "Ember service", "Ember project", "ember.js".
Input Context
Before activating, verify:
- package.json has ember-source or ember-cli.
- Whether the project uses Ember Octane (v3.15+) or classic (v3.14-).
- Whether Ember Data or other data layer (Apollo, fetch).
Output Artifact
No file output. Produces code snippets and structural guidance as text.
Response Format
Code with Ember conventions:
// app/routes/index.ts
import Route from '@ember/routing/route'
import { service } from '@ember/service'
No preamble. No postamble. No explanations. Compress output — why use many token when few do trick.
Completion Criteria
- Routes defined in app/router.ts with corresponding route files.
- Templates use Ember/Glimmer syntax ({{each}}, {{if}}).
- Components are Glimmer components (native class, or template-only).
- Ember Data models defined with @attr, @belongsTo, @hasMany.
- Services for shared state with @service injection.
- Modifiers for DOM interactions (ember-render-modifiers or custom).
- Tests for routes, components, and services.
Max Response Length
~4096 tokens.
Component Architecture / Decision Trees
Architecture Options
| Approach | Trade-off | When to Use |
|---|---|---|
| Glimmer component (native class) | Full lifecycle, tracked state | Interactive components with logic |
| Template-only (.hbs) | Zero JS, pure display | Static UI, presentational |
| Classic component | Legacy, deprecated | Old code, migration path only |
| Custom modifier | DOM interaction only | Event listeners, scroll handling |
| Helper function | Pure transformations | Formatting, math, string utils |
Component Type Decision
What is the component's purpose?
Pure display (no logic, no state) -> Template-only component (.hbs only)
Display with local state -> Glimmer component (native class + @tracked)
Encapsulated behavior + template -> Classic component (deprecated, avoid)
Reusable DOM interaction -> Custom modifier (use:)
Contextual helper -> Helper function (pure, no state)
State Management Decision
Where does the data live?
Per-component, ephemeral -> @tracked property on the component
Shared across routes -> Route model hook + service caching
Global application state -> Service (@service injection)
Persisted server data -> Ember Data store (this.store.findAll)
URL state -> Query params on controller
Data Loading Decision
Where is the data needed?
Single route -> model() hook in route
Multiple routes sharing data -> Parent route model + modelFor on child
Global (user, session) -> Service loaded in application route
Lazy / on-demand -> this.store.findRecord in component (Octane)
Real-time -> Ember Concurrency or tracked tasks
Component Design Patterns
Route with Model Hook
// app/routes/posts.ts
import Route from '@ember/routing/route'
import { service } from '@ember/service'
export default class PostsRoute extends Route {
@service declare store: Store
async model(params: { page: string }) {
const page = parseInt(params.page) || 1
return this.store.findAll('post', { page })
}
}
Glimmer Component with Tracked
// app/components/post-card.ts
import Component from '@glimmer/component'
import { tracked } from '@glimmer/tracking'
import { action } from '@ember/object'
interface Args {
post: Post
onSelect: (id: string) => void
}
export default class PostCard extends Component<Args> {
@tracked isExpanded = false
get excerpt() {
return this.args.post.body.length > 100
? this.args.post.body.slice(0, 100) + '...'
: this.args.post.body
}
@action
toggleExpand() {
this.isExpanded = !this.isExpanded
}
@action
select() {
this.args.onSelect(this.args.post.id)
}
}
{{! app/components/post-card.hbs }}
<article class="post-card">
<h2>{{@post.title}}</h2>
<p>{{if this.isExpanded @post.body this.excerpt}}</p>
<button type="button" {{on "click" this.toggleExpand}}>
{{if this.isExpanded "Show less" "Show more"}}
</button>
</article>
Template-Only Component
{{! app/components/ui/button.hbs }}
<button class="btn btn--{{@variant}}" type="button" ...attributes>
{{yield}}
</button>
Service with Ember Data
// app/services/post-store.ts
import Service from '@ember/service'
import { service } from '@ember/service'
import { tracked } from '@glimmer/tracking'
export default class PostStoreService extends Service {
@service declare store: Store
@tracked currentPostId: string | null = null
get currentPost() {
return this.currentPostId
? this.store.peekRecord('post', this.currentPostId)
: null
}
async findByTag(tag: string) {
return this.store.query('post', { tag })
}
async createPost(data: Partial<Post>) {
const post = this.store.createRecord('post', data)
await post.save()
return post
}
}
Custom Modifier
// app/modifiers/click-outside.ts
import Modifier from 'ember-modifier'
interface Args {
positional: []
named: { action: () => void }
}
export default class ClickOutsideModifier extends Modifier<Args> {
private handler: ((e: MouseEvent) => void) | null = null
didReceiveArguments() {
this.handler = (e: MouseEvent) => {
if (!this.element.contains(e.target as Node)) {
this.args.named.action()
}
}
document.addEventListener('click', this.handler, true)
}
willRemove() {
if (this.handler) {
document.removeEventListener('click', this.handler, true)
}
}
}
State Management Patterns
Ember Data Store
// Fetching
const posts = await this.store.findAll('post')
const post = await this.store.findRecord('post', id)
const results = await this.store.query('post', { category: 'tech' })
// Creating
const post = this.store.createRecord('post', { title: 'New', body: '...' })
await post.save()
// Updating
post.title = 'Updated'
await post.save()
// Deleting
await post.destroyRecord()
Service-Based State
Services are singletons injected with @service. Use @tracked for reactive properties:
@service declare auth: AuthService
@service declare cart: CartService
// In template: {{this.auth.user.name}}
URL State via Query Params
// app/controllers/posts.ts
import Controller from '@ember/controller'
import { tracked } from '@glimmer/tracking'
export default class PostsController extends Controller {
@tracked page = 1
queryParams = ['page']
}
Performance Optimization
Rendering Performance
- Glimmer VM is the fastest Ember rendering engine — Octane apps render 3-5x faster than classic.
- @tracked properties enable fine-grained reactivity — only dependent DOM sections re-render.
- Template-only components have zero JS overhead.
- Angle bracket syntax () is faster than classic {{my-component}} invocation.
Bundle Size
- Ember base: ~100KB gzipped (larger than React/Vue due to built-in features).
- Lazy loading via ember-engines for route-based code splitting.
- Tree-shake by removing unused addons from package.json.
- Use
ember-cli-bundle-analyzerto inspect bundle composition.
Optimization Techniques
- Use
trackedovercomputedfor local state — computed has caching overhead. - Avoid creating new objects/arrays in tracked getters — use cached references.
- Use
ember-concurrency'sdropTaskfor rapidly firing events (typeahead). - Debounce expensive operations with
ember-concurrency'srestartableTask. - Virtual scrolling for large lists with
ember-collectionorvertical-collection.
Build & Bundle Considerations
- Ember CLI uses Broccoli.js as the build pipeline.
- Addons add to bundle size — audit
package.jsonperiodically. - Use
ember-auto-importfor npm package imports. ember-cli-code-coveragefor tracking unused code.- Lazy load engines with
ember-enginesfor large feature areas. - Production builds:
ember build --environment=productionenables minification and tree-shaking.
Testing Strategies
Component Tests
// tests/integration/components/post-card-test.ts
import { module, test } from 'qunit'
import { setupRenderingTest } from 'ember-qunit'
import { render, click } from '@ember/test-helpers'
import { hbs } from 'ember-cli-htmlbars'
module('Integration | Component | post-card', function (hooks) {
setupRenderingTest(hooks)
test('it toggles expanded state', async function (assert) {
this.set('title', 'Test Post')
this.set('body', 'Test body')
await render(hbs`<PostCard @title={{this.title}} @body={{this.body}} />`)
assert.dom('article').exists()
await click('h2')
assert.dom('p').hasText('Test body')
})
})
Route Tests
module('Acceptance | posts', function (hooks) {
setupApplicationTest(hooks)
test('visiting /posts loads data', async function (assert) {
await visit('/posts')
assert.dom('[data-test-post]').exists({ count: 10 })
})
})
Key Testing Practices
- Use
ember-qunitwith@ember/test-helpersfor DOM interaction. - Use
ember-cli-miragefor mocking Ember Data responses. - Prefer integration tests over unit tests for components.
- Use
settled()after async operations to wait for rendering.
Migration Patterns
Classic (v3.14-) to Octane (v3.15+)
| Classic | Octane |
|---|---|
EmberObject.extend() |
Native class extends Component |
computed() |
@tracked + getter |
.observes() |
@tracked + @action |
didInsertElement |
{{did-insert}} modifier |
this.set('prop', val) |
this.prop = val |
{{my-component}} |
<MyComponent /> |
this._super() |
No equivalent (native class) |
Ember.Component |
@glimmer/component |
Migration order: 1) Update Ember CLI to v3.15+, 2) Convert components one by one, 3) Replace computed with @tracked, 4) Replace observers with native getters, 5) Use ember-cli-update for automated migration.
From React to Ember
| React Pattern | Ember Equivalent |
|---|---|
useState |
@tracked property |
useEffect |
Modifiers ({{did-insert}}, {{did-update}}) |
useContext |
@service injection |
| Props | @arg (named args in .hbs template) |
| JSX | Handlebars (.hbs) templates |
React.memo |
Template-only component (no JS class) |
Anti-Patterns
- Using classic components for new code: Always use Glimmer components.
- Mutating @tracked with set():
set()is for classic mode. Octane usesthis.property = value. - Over-nesting routes: Each nesting adds a template + controller + route file.
- Missing outlet in layout templates: Child routes won't render without {{outlet}}.
- Service as a data dump: Services should encapsulate logic.
- Direct DOM manipulation: Use modifiers or {{did-insert}} instead of lifecycle hooks.
- Not using ember-concurrency for async: Raw promises in @tracked cause memory leaks.
- Over-using observers: Use @tracked + getters/computed.
Common Pitfalls
- Missing outlet in layout templates — {{outlet}} is required for child routes.
- Forgetting to import types — Ember's TS support requires explicit type imports.
- Using classic components for new code — always Glimmer.
- Mutating @tracked with set() — use
this.prop = value. - Service as a data dump — encapsulate logic in services.
- Direct DOM manipulation in components — use modifiers.
Compared With
| Aspect | Ember | React | Vue |
|---|---|---|---|
| Architecture | Convention over config | Library + choices | Framework |
| Build tool | Ember CLI | Vite/CRA | Vite/Vue CLI |
| State mgmt | Ember Data + Services | Zustand/Redux | Pinia/Vuex |
| Templating | Handlebars (.hbs) | JSX/TSX | .vue SFC |
| Routing | Built-in, config | React Router | Vue Router |
| TypeScript | First-class since v4 | Optional | Via vue-tsc |
| Learning curve | Steep | Moderate | Moderate |
Ecosystem & Tooling
- Ember CLI —
ember generate component,ember generate route - Ember Inspector browser extension
ember-cli-mirage— API mockingember-concurrency— async task managementember-truth-helpers— boolean template helpersember-composable-helpers— functional helpersember-test-selectors— data-test-* stripped from productionember-cli-update— automated migrationember-template-lint— template lintingember-cli-bundle-analyzer— bundle analysis
UI Libraries
- ember-cli-addon-docs — Component documentation
- ember-paper — Material Design
- ember-bootstrap — Bootstrap integration
- ember-power-select — Advanced select
Rules
- Use Ember CLI commands for scaffolding — never write boilerplate by hand.
- Routes define model hooks; controllers only for query params or actions.
- Glimmer components ( or .hbs with .ts) for all new code.
- Use
@trackedfor reactive properties, neverset(). - Use
@actiondecorator for event handlers. - Ember Data is the default data layer — use adapters/serializers for API customization.
- Services are singletons — inject with
@service. - Modifiers handle DOM interactions, not component lifecycle hooks.
- Follow the
app/folder convention: routes/, components/, services/, models/, modifiers/.
References
- references/ember-advanced.md — Ember Advanced Topics
- references/ember-architecture.md — Ember.js Architecture Patterns
- references/ember-deployment.md — Ember.js Deployment
- references/ember-fundamentals.md — Ember Fundamentals
- references/ember-patterns.md — Ember.js Patterns & Best Practices
- references/ember-setup.md — Ember.js Setup Guide
Handoff
No artifact produced. Next skill: ember-data (if complex data layer) or frontend-testing. Carry forward: route/service pattern, Glimmer component conventions, @tracked/@action.
Implementation Patterns
Factory Pattern for Module Creation
function createModule<T>(config: ModuleConfig): T { const dependencies = initializeDependencies(config); const module = new Module(dependencies); module.hooks.onInit(); return module as T; }
Builder Pattern for Complex Configuration
class ConfigBuilder { private config: AppConfig = new AppConfig(); withDatabase(url: string): ConfigBuilder { ... } withCache(ttl: number): ConfigBuilder { ... } withLogging(level: string): ConfigBuilder { ... } build(): AppConfig { return this.config; } }
Production Considerations
Deployment Checklist
- Production build with optimizations enabled
- Environment variables configured per environment
- Health check endpoint responds correctly
- Error tracking and monitoring integrated
- Logging level configured (not debug in production)
- Resource limits configured
- Database migrations applied
- Static assets built and served from CDN or cache
- Feature flags toggled appropriately
- Rollback plan documented and tested
Monitoring and Alerting
| Metric | Threshold | Severity | Action |
|---|---|---|---|
| Error rate | > 1% | Critical | Rollback or fix |
| p95 latency | > 500ms | Warning | Profile and optimize |
| Uptime | < 99.9% | Critical | Investigate infrastructure |
| Memory usage | > 80% | Warning | Check for leaks |
| CPU usage | > 80% | Warning | Scale up or optimize |
Architecture Decision Trees
Component Strategy Decision Tree
Does the component need lifecycle hooks or DOM access?
├── No → Template-only component (Glimmer) - fastest, no JS class
└── Yes → Does it manage state shared across components?
├── Yes → Service + component pattern
└── No → Classic Glimmer component with @tracked
Is the DOM interaction imperative (drag/drop, canvas)?
├── Yes → Use modifier (ember-modifier)
└── No → Use component lifecycle (constructor, willDestroy)
Routing Strategy Decision Tree
Is the route data-driven (model-dependent)?
├── No → Static route with template content
└── Yes → Does the model come from API?
├── Yes → Route model() hook with ember-data or fetch
└── No → Route model() with local computation
Are there multiple async dependencies?
├── Yes → RSVP.hash() in model() with parallel loading
└── No → Single async model() return
Security Considerations
- Safe strings: Use
{{someProperty}}for auto-escaped output. For trusted HTML, use{{{htmlContent}}}only after sanitization viaember-cli-htmlbarsor DOMPurify. Never triple-stash user content. - CSRF protection: Ember Data automatically reads CSRF token from meta tag. Ensure backend sets
<meta name="csrf-token" content="...">. For non-ember-data requests, read the meta tag and include in headers. - Content Security Policy: Configure CSP in
config/content-security-policy.js. Ember'sember-cli-buildcan inject meta CSP tags. Setscript-src 'self'and use nonces for inline scripts in production. - Dependency auditing: Run
ember-cli-deprecation-workflowto track deprecations. Usenpm auditoryarn auditin CI. Pin major dependency versions. Avoid deprecated Ember addons without active maintenance.