Dependency Injection
Dependency injection (DI) in our Typescript clients is manually configured across several locations.
You will need to update the DI configuration when you create a new service or change the dependencies injected into an existing service.
Angular and Non-Angular contexts
Our Typescript clients use both Angular and non-Angular contexts. This can vary:
- between apps - for example, the web client is entirely Angular, whereas the CLI client does not use Angular at all
- within apps - for example, the browser client uses Angular in its popup but not in its background page/service worker
This has important consequences for how we write services and how we configure DI. In particular:
- Angular contexts use the Angular dependency injection framework
- non-Angular contexts do not use any dependency injection framework and must manually instantiate their dependencies
The overall effect of these items is that we use Angular DI in a non-standard way. It forces
interfaces to be abstract classes and requires that we manually wire up dependencies in service
modules, rather than relying on Inject
decorators that would import Angular dependencies into
non-Angular contexts.
Services
Services should be organized as follows depending on where they're used:
libs/common
for services used by multiple clients in non-Angular contexts (or a mix of Angular and non-Angular contexts)libs/angular
for services used by multiple clients in Angular contexts onlyapps/<client-name>
for services used by a single client only
Some teams also have their own libs
folder, which are structured similarly. e.g. libs/auth
,
containing libs/auth/common
and libs/auth/angular
. These should be used if available.
If a service is used in an Angular context only (e.g. app/<angular-only-client>
or
libs/angular
), it can use the Angular @Injectable
decorator to automatically configure its
dependencies. If a service is used in mixed contexts (e.g. libs/common
), it must not use Angular
decorators and its dependencies must be manually configured.
Interfaces
A service should implement an interface defined by an abstract class if:
- The service will be used across multiple client apps, or
- You want to define multiple interfaces for your service to limit what functionality is available to different consumers
For example, you may want to use interfaces to prevent outside modules from calling your internal methods:
// Injected in other modules
abstract class MyService {
abstract externalMethod(): void;
}
// Injected in the MyService feature module
abstract class InternalMyService extends MyService {
abstract internalMethod(): void;
}
class MyService implements InternalMyService {
abstract externalMethod(): void;
abstract internalMethod(): void;
}
If neither of these apply to you, you probably don't need to define an abstract interface for your service.
Configuring DI
Angular modules
Angular contexts generally use ngModules dependency providers to configure DI. For example:
This is a simplified example only - read on to learn about safeProvider.
@NgModule({
providers: [
// If the service does not have @Injectable decorators
{
provide: MyService, // abstract interface
useClass: DefaultMyService, // concrete implementation
deps: [DepA, DepB], // abstract interfaces for each dependency required by the DefaultMyService constructor
},
// If the service does have @Injectable decorators (Angular-only)
{
provide: SomeAngularService,
useClass: DefaultSomeAngularService,
// no deps array required
},
],
})
export class MyServicesModule {}
By default, this is not type safe - Angular does not verify that the implementation actually
implements the abstract interface, or that the deps
array matches the constructor parameters.
We provide a helper function called safeProvider
, which acts as a wrapper around each provider
entry and provides compile-time type checking of your configuration. For example (continuing with
the example above):
safeProvider
is the preferred way to register services in ngModules.
@NgModule({
providers: [
safeProvider({
provide: MyService,
useClass: DefaultMyService,
deps: [DepA, DepB],
}),
safeProvider({
provide: SomeAngularService,
useClass: DefaultSomeAngularService,
useAngularDecorators: true, // this tells safeProvider to not require a deps array
}),
],
})
export class MyServicesModule {}
The useAngularDecorators
option is not type safe - it is your responsibility to ensure that the
class actually uses the @Injectable
decorator.
Our primary service modules require the use of safeProvider
, otherwise you will receive type
errors. However, you should use safeProvider
wherever you are configuring an Angular provider to
benefit from its type checking.
Angular root module
Services that will be used globally and which don't have an abstract interface can be registered
using the provideIn
property. This avoids having to create and manage service modules. When
exporting services through feature modules, it can be tricky to manage their lifetime, and it's easy
to end up with multiple instances of the service being created.
@Injectable({
provideIn: "root",
})
export class MyService {}
Angular components
Services that are not stateful and/or are only supposed to be used locally to extract complex logic from a component can be injected directly into standalone components, completely bypassing modules.
@Component({
providers: [
// This example assumes that MyService uses @Injectable decorators and does not have an abstract interface
safeProvider(MyService),
],
standalone: true,
})
export class MyComponent {}
Non-Angular contexts
Non-Angular contexts do not use a DI framework and must therefore manually instantiate their dependencies in the correct order. Instantiating dependencies out of order may result in null values being injected into services, causing runtime errors.
DI by client
Shared Angular DI
libs/angular
contains JslibServicesModule
, a services module which registers common dependencies
used across Angular Typescript clients (web, browser and desktop).
A client may override this DI if a client-specific configuration is required.
Web
core.module.ts
contains the main services module for the web client. It imports
JslibServicesModule
.
However, the web module heavily uses feature modules and standalone components to divide its code by feature. Feature modules and standalone components should configure their own DI wherever possible for feature-specific (non-global, non-stateful) services.
Browser
Browser DI is split across the following locations:
services.module.ts
is the main services module for the browser extension popup. It importsJslibServicesModule
main.background.ts
manually instantiates services used in the background page (for Manifest v2) or the service worker (for Manifest v3). It does not use any dependency injection framework- a series of exported factory functions with the naming convention
[name]-service.factory.ts
. These were used early in development for Manifest v3, but will be removed in the near future
The background page/service worker still does a lot of work in the browser extension, so many services are registered in all the above locations.
Desktop
Desktop DI is split across the following locations:
services.module.ts
is the main services module for the Electron renderer process, which is responsible for bootstrapping and rendering the Bitwarden app. It importsJslibServicesModule
main.ts
manually instantiates services used in the main process. It does not use any dependency injection framework
The main process is primarily used for Electron behavior and native integrations (e.g. storage, biometrics, window behavior), so it registers a smaller subset of services. Many services are only registered in the renderer process.
CLI
bw.ts
contains all DI for the CLI client. It does not use any dependency injection framework.