Skip to content

Aurelia Dependency Injection — Managing Services and Dependencies

DodaTech Updated 2026-06-28 5 min read

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

  1. Creating manual singletons with static properties. Do not manage singletons yourself. Register the class with @singleton() and let the container handle it.
  2. Circular dependencies. Service A depends on B, B depends on A. Use Container injection or restructure to break the cycle.
  3. Forgetting @inject when using plain JavaScript. Without TypeScript decorators, @inject is required to tell Aurelia what to inject.
  4. Injecting too many services. A component with 5+ injected services is doing too much. Decompose into smaller services or components.
  5. Not clearing service state in tests. Singletons persist state across tests. Use container.unregister() or recreate the container for each test.

Practice Questions

  1. What does @inject do?
  2. What is the difference between singleton and transient?
  3. How do you register a global service?
  4. How do you mock a service in tests?
  5. Challenge: Create a service architecture for an authentication system: AuthService (login, logout, token refresh), ApiClient (HTTP requests with auth headers), TokenStorage (localStorage management), and UserStore (user state). Inject them into components. Write unit tests with mocked dependencies.

FAQ

Is DI mandatory in Aurelia?

No, but it is the recommended pattern. You can create instances with new directly.

Can I inject into plain classes?

Yes. Use @inject on any class. The container resolves dependencies during construction.

What happens if a dependency is not registered?

The container throws a DependencyResolutionError. Register missing dependencies.

Can I use DI with third-party libraries?

Yes. Register them as instances: container.registerInstance(LibName, new LibName()).

How do I control singleton disposal?

Singletons live for the application lifetime. Implement dispose() for cleanup.

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