Aurelia Dependency Injection — Managing Services and Dependencies
In this tutorial, you will learn about Aurelia Dependency Injection. We cover key concepts, practical examples, and best practices to help you master this topic.
Aurelia dependency injection is a built-in system for managing service instances. The DI container resolves constructor parameters automatically, manages singleton and transient lifetimes, and supports hierarchical containers for scoped dependencies.
What You'll Learn
You will learn how to use @inject, register services, manage singleton vs transient instances, use factory patterns, and test components with mocked dependencies.
Why It Matters
DI decouples service creation from usage. Components do not create services — they declare what they need and receive them. This makes code testable, flexible, and maintainable.
Real-World Use
An Aurelia application injects an API client service into all components that need server data. During testing, the same components receive a mock API client. No component code changes between production and test.
flowchart LR
A[DI Container] --> B[Singleton Services]
A --> C[Transient Services]
A --> D[Factory Functions]
B --> E[SessionService]
B --> F[ConfigService]
C --> G[HttpClient]
D --> H[Dynamic instances]
E --> I[Component A]
E --> J[Component B]
Basic Injection
import { inject } from 'aurelia-framework';
import { HttpClient } from 'aurelia-fetch-client';
@inject(HttpClient)
export class UserService {
constructor(http) {
this.http = http;
}
async getUsers() {
const response = await this.http.fetch('/api/users');
return response.json();
}
}
Multiple Dependencies
import { inject } from 'aurelia-framework';
import { HttpClient } from 'aurelia-fetch-client';
import { EventAggregator } from 'aurelia-event-aggregator';
import { Router } from 'aurelia-router';
@inject(HttpClient, EventAggregator, Router)
export class AuthService {
constructor(http, eventBus, router) {
this.http = http;
this.eventBus = eventBus;
this.router = router;
}
async login(credentials) {
// Use http, eventBus, and router
}
}
Auto-Injection (Without Decorator)
Aurelia supports auto-injection by type when TypeScript is used with emitDecoratorMetadata.
// Requires TypeScript with emitDecoratorMetadata: true
import { HttpClient } from 'aurelia-fetch-client';
export class ApiService {
constructor(private http: HttpClient) {
// No @inject needed
}
}
Registering Services
Services are registered in main.ts or in feature modules.
// src/main.ts
import { Aurelia } from 'aurelia-framework';
export function configure(aurelia) {
aurelia.use.standardConfiguration();
// Register as singleton (one instance for the app)
aurelia.container.registerSingleton(MyService);
// Register as transient (new instance each injection)
aurelia.container.registerTransient(HttpClient);
// Register with explicit key
aurelia.container.registerSingleton(AuthService, AuthService);
aurelia.start().then(() => aurelia.setRoot(PLATFORM.moduleName('app')));
}
Singleton via Class Decorator
import { singleton } from 'aurelia-framework';
@singleton()
export class ConfigService {
get(key) {
return localStorage.getItem(key);
}
set(key, value) {
localStorage.setItem(key, value);
}
}
Transient Registration
import { transient } from 'aurelia-framework';
@transient()
export class ApiRequest {
constructor() {
this.requestId = Date.now();
this.createdAt = new Date();
}
}
Each time ApiRequest is injected, a new instance is created.
Factory Pattern
import { inject } from 'aurelia-framework';
@inject(ApiService)
export class DataRepository {
constructor(api) {
this.api = api;
}
forEndpoint(endpoint) {
return new EndpointRepository(this.api, endpoint);
}
}
class EndpointRepository {
constructor(api, endpoint) {
this.api = api;
this.endpoint = endpoint;
}
async getAll() {
return this.api.get(`/${this.endpoint}`);
}
}
Hierarchical Containers
Child containers inherit from parent containers. Components can have their own container scope.
// Child container for a specific area
const childContainer = parentContainer.createChild();
// Register a module-specific service
childContainer.registerTransient(ModuleService);
// ModuleService is only available in this child scope
Service Lifecycle
import { singleton, inject } from 'aurelia-framework';
@singleton()
export class SessionService {
constructor() {
this.user = null;
this.token = null;
console.log('SessionService created (once)');
}
login(user, token) {
this.user = user;
this.token = token;
this.saveToStorage();
}
logout() {
this.user = null;
this.token = null;
this.clearStorage();
}
get isAuthenticated() {
return !!this.token;
}
}
Testing with DI
// tests/unit/user-service-test.js
import { Container } from 'aurelia-framework';
import { UserService } from 'src/services/user-service';
describe('UserService', () => {
let container;
let mockHttp;
let userService;
beforeEach(() => {
container = new Container();
// Mock HttpClient
mockHttp = {
fetch: jasmine.createSpy('fetch')
};
// Register mock
container.registerInstance(HttpClient, mockHttp);
// Resolve service with mock dependency
userService = container.get(UserService);
});
it('should fetch users from API', async () => {
mockHttp.fetch.and.returnValue(
Promise.resolve({ json: () => [{ id: 1, name: 'Alice' }] })
);
const users = await userService.getUsers();
expect(mockHttp.fetch).toHaveBeenCalledWith('/api/users');
expect(users.length).toBe(1);
expect(users[0].name).toBe('Alice');
});
});
Custom Resolution
import { inject, Container } from 'aurelia-framework';
// Resolve dynamically at runtime
export class DynamicService {
constructor(container) {
this.container = container;
}
getService(type) {
return this.container.get(type);
}
createInstance(type, ...args) {
return this.container.createInstance(type, args);
}
}
Common Mistakes
- Creating manual singletons with static properties. Do not manage singletons yourself. Register the class with
@singleton()and let the container handle it. - Circular dependencies. Service A depends on B, B depends on A. Use
Containerinjection or restructure to break the cycle. - Forgetting
@injectwhen using plain JavaScript. Without TypeScript decorators,@injectis required to tell Aurelia what to inject. - Injecting too many services. A component with 5+ injected services is doing too much. Decompose into smaller services or components.
- Not clearing service state in tests. Singletons persist state across tests. Use
container.unregister()or recreate the container for each test.
Practice Questions
- What does
@injectdo? - What is the difference between singleton and transient?
- How do you register a global service?
- How do you mock a service in tests?
- Challenge: Create a service architecture for an authentication system:
AuthService(login, logout, token refresh),ApiClient(HTTP requests with auth headers),TokenStorage(localStorage management), andUserStore(user state). Inject them into components. Write unit tests with mocked dependencies.
FAQ
Mini Project
Build a service layer for an e-commerce application: (1) ProductService — fetch products, search, filter. (2) CartService — add, remove, update quantity, calculate totals. (3) OrderService — place order, track status. (4) AuthService — login, register, manage tokens. All services use DI. Inject them into components. Write unit tests with mocked HTTP calls.
What's Next
Now that you understand DI, learn Aurelia Routing for navigation. Then explore Aurelia Router Lifecycle for route guards.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro