DI & Providers
Basically, almost everything may be considered as a provider – service, factory, interceptors, and so on. All of them can inject dependencies, meaning, they can create various relationships with each other. But in fact, a provider is nothing else than just a simple class annotated with an @Injectable()
decorator.
In controllers chapter, we've seen how to build a Controller, handle a request and create a response. Controllers shall handle HTTP requests and delegate complex tasks to the providers.
Providers are plain javascript classes and use one of these decorators on top of them. Here is the list:
TIP
Since v8, you can also use the functional API to define your providers using the injectable function. This function lets you define your provider without using decorators and lets you define your provider in a more functional way.
This page will show you how to use both API to define your providers.
Injectable
Let's start by creating a simple CalendarService provider.
import {Service} from "@tsed/di";
import {Calendar} from "../models/Calendar.js";
@Service()
export class CalendarsService {
private readonly calendars: Calendar[] = [];
create(calendar: Calendar) {
this.calendars.push(calendar);
}
findAll(): Calendar[] {
return this.calendars;
}
}
import {injectable} from "@tsed/di";
import {Calendar} from "../models/Calendar.js";
export class CalendarService {
private readonly calendars: Calendar[] = [];
create(calendar: Calendar) {
this.calendars.push(calendar);
}
findAll(): Calendar[] {
return this.calendars;
}
}
injectable(CalendarsService);
Service and Injectable have the same effect. Injectable accepts options, Service does not. A Service is always configured as
singleton
.
Example with Injectable:
import {Injectable, ProviderScope, ProviderType} from "@tsed/di";
import {Calendar} from "../models/Calendar.js";
@Injectable({
type: ProviderType.SERVICE,
scope: ProviderScope.SINGLETON
})
export class CalendarsService {
private readonly calendars: Calendar[] = [];
create(calendar: Calendar) {
this.calendars.push(calendar);
}
findAll(): Calendar[] {
return this.calendars;
}
}
import {ProviderScope, ProviderType} from "@tsed/di";
import {Calendar} from "../models/Calendar.js";
export class CalendarsService {
private readonly calendars: Calendar[] = [];
create(calendar: Calendar) {
this.calendars.push(calendar);
}
findAll(): Calendar[] {
return this.calendars;
}
}
injectable(CalendarsService)
.type(ProviderType.SERVICE)
.scope(ProviderScope.SINGLETON);
Now we have the service class already done, let's use it inside the CalendarsController
:
import {BodyParams} from "@tsed/platform-params";
import {Get, Post} from "@tsed/schema";
import {Controller} from "@tsed/di";
import {Calendar} from "../models/Calendar.js";
import {CalendarsService} from "../services/CalendarsService.js";
@Controller("/calendars")
export class CalendarsController {
@Inject()
private readonly calendarsService: CalendarsService
@Post()
create(@BodyParams() calendar: Calendar) {
return this.calendarsService.create(calendar);
}
@Get()
findAll(): Promise<Calendar[]> {
return this.calendarsService.findAll();
}
}
import {BodyParams} from "@tsed/platform-params";
import {Get, Post} from "@tsed/schema";
import {controller} from "@tsed/di";
import {Calendar} from "../models/Calendar.js";
import {CalendarsService} from "../services/CalendarsService.js";
export class CalendarsController {
private readonly calendarsService = inject(CalendarsService);
@Post()
create(@BodyParams() calendar: Calendar) {
return this.calendarsService.create(calendar);
}
@Get()
findAll(): Promise<Calendar[]> {
return this.calendarsService.findAll();
}
}
controller(CalendarsController)
.path("/calendars");
import {PlatformTest} from "@tsed/platform-http/testing";
import {Calendar} from "../models/Calendar.js";
import {CalendarsService} from "../services/CalendarsService.js";
async function getFixture() {
const service = {
findAll: vi.fn().mockResolvedValue([]),
create: vi.fn().mockImplementation((calendar: Calendar) => {
calendar.id = "id";
return calendar;
})
};
const controller = await PlatformTest.invoke<CalendarsController>(CalendarsController, [
{
token: CalendarsService,
use: service
}
]);
return {
service,
controller
};
}
describe("CalendarsController", () => {
beforeEach(() => PlatformTest.create());
afterEach(() => PlatformTest.reset());
describe("findAll()", () => {
it("should return calendars from the service", async () => {
const {controller, service} = await getFixture();
const result = await controller.findAll();
expect(result).toEqual([]);
expect(service.findAll).toHaveBeenCalledWith();
});
});
describe("create()", () => {
it("should create using the service", async () => {
const {controller, service} = await getFixture();
const calendar = new Calendar();
const result = await controller.create(calendar);
expect(result.id).toEqual(id);
expect(service.create).toHaveBeenCalledWith(calendar);
});
});
});
Functional API doesn't provides alternative for Get and BodyParams decorators at the moment.
Finally, we can load the injector and use it:
import {Configuration} from "@tsed/di";
// Note: .js extension is required when using ES modules
import {CalendarsController} from "./controllers/CalendarsController.js";
@Configuration({
mount: {
"/rest": [CalendarsController]
}
})
export class Server {}
import {configuration} from "@tsed/di";
import {CalendarsController} from "./controllers/CalendarsController.js";
export class Server {
}
configuration(Server, {
mount: {
"/rest": [CalendarsController]
}
});
NOTE
You'll notice that we only import the CalendarsController and not the CalendarsService as that would be the case with other DIs (Angular / inversify). Ts.ED will discover automatically services/providers as soon as it's imported into your application via an import ES6.
In most case, if a service is used by a controller or another service which is used by a controller, it's not necessary to import it explicitly!
Dependency injection
Ts.ED is built around the dependency injection pattern. TypeScript emits type metadata on the constructor which will be exploited by the InjectorService to resolve dependencies automatically.
import {Injectable} from "@tsed/di";
@Injectable()
class MyInjectable {
@Inject()
private calendarsService: CalendarsService;
// or through the constructor
constructor(private calendarsService2: CalendarsService) {
console.log(calendarsService);
console.log(calendarsService2);
console.log(calendarsService === calendarsService2); // true
}
}
import {Injectable} from "@tsed/di";
@Injectable()
class MyInjectable {
private calendarsService = inject(CalendarsService);
constructor() {
const calendarsService2 = inject(CalendarsService);
console.log(calendarsService);
console.log(calendarsService2);
console.log(calendarsService === calendarsService2); // true
}
}
Important
With v7, accessing to a property decorated with Inject(), Constant, Value in the constructor is not possible. You have to use the $onInit()
hook to access to the injected service.
import {Injectable, Inject} from "@tsed/di";
@Injectable()
class MyInjectable {
@Inject()
private calendarsService: CalendarService;
$onInit() {
console.log(this.calendarsService);
}
}
The v8 solve that, and now, you can access to the injected service in the constructor.
Note that, the inject function can be used anywhere in your code, not only in a DI context:
function getCalendarsService(): CalendarsService {
const calendarsService = inject(CalendarsService);
console.log(calendarsService);
// do something
return calendarsService;
}
Scopes
All providers have a lifetime strictly dependent on the application lifecycle. Once the server is created, all providers have to be instantiated. Similarly, when the application shuts down, all providers will be destroyed. However, there are ways to make your provider lifetime request-scoped as well. You can read more about these techniques here.
Binding configuration
All configurations set with Module or Configuration can be retrieved with Constant and Value decorators, or with configuration, constant, and refValue functions. It supports lodash path to retrieve nested properties.
By using these decorators or functions, you can more easily decouple your code from your server configurations and therefore more easily test your code independently of environment variables managed by files (.env, nconf, etc.).
Instead of doing this:
import {Injectable} from "@tsed/di";
@Injectable()
class MyService {
doSomething() {
const value = process.env.MY_VALUE;
// your code
}
}
Do this:
import {Configuration, Constant, Injectable} from "@tsed/di";
@Injectable()
class MyService {
@Constant("envs.MY_VALUE")
private value: string;
doSomething() {
console.log(this.value);
// your code
}
}
// server.ts
@Configuration({
envs: {
MY_VALUE: process.env.MY_VALUE || "myValue"
}
})
class Server {
}
class MyService {
private value = constant<string>("envs.MY_VALUE");
doSomething() {
console.log(this.value);
// your code
}
}
injectable(MyService);
// server.ts
configuration(Server, {
envs: {
MY_VALUE: process.env.MY_VALUE || "myValue"
}
});
Then, you can test your code like this:
import {PlatformTest} from "@tsed/platform-http/testing";
import {inject} from "@tsed/di";
import {MyService} from "./MyService.js";
describe("MyService", () => {
describe("when MY_VALUE is given", () => {
beforeEach(() =>
PlatformTest.create({
envs: {
MY_VALUE: "myValue"
}
})
);
afterEach(() => PlatformTest.reset());
it("should do something", () => {
const myService = inject(MyService);
expect(myService.doSomething()).toBe("myValue");
});
});
describe("when MY_VALUE IS undefined", () => {
beforeEach(() => PlatformTest.create());
afterEach(() => PlatformTest.reset());
it("should do something", () => {
const myService = inject(MyService);
expect(myService.doSomething()).toBe("myValue");
});
});
});
Testing different configurations is now much easier.
Constant
The Constant decorator or constant function is used to inject a constant value into a provider.
import {Env} from "@tsed/core";
import {Constant, Injectable} from "@tsed/di";
@Injectable()
export class MyClass {
@Constant("env")
private readonly env: Env;
constructor() {
console.log(this.env);
}
}
// server.ts
import {Configuration} from "@tsed/di";
@Configuration({
env: process.env.NODE_ENV
})
class Server {}
import {Env} from "@tsed/core";
import {constant} from "@tsed/di";
export class MyClass {
private readonly env = constant<Env>(Env);
constructor() {
console.log(this.env);
}
}
injectable(MyClass);
// server.ts
import {configuration} from "@tsed/di";
class Server {}
configuration(Server, {
env: process.env.NODE_ENV
})
WARNING
Constant returns an immutable value using Object.freeze()
. If you need to inject a mutable value, use Value instead.
Note that, constant function can be used anywhere in your code, not only in a DI context:
function getMyValue(): string {
const value = constant<string>("MY_VALUE");
console.log(value); // "myValue"
// do something
return value;
}
@Injectable()
class MyService {
constructor() {
const value = getMyValue();
console.log(value); // "myValue"
}
}
// server.ts
import {configuration} from "@tsed/di";
class Server {}
configuration(Server, {
MY_VALUE: "myValue"
});
Default value can be set with the second argument of the Constant / constant:
@Constant("MY_VALUE", "defaultValue")
constant("MY_VALUE", "defaultValue");
Note
constant try to infer the type from the default value. But sometimes, TypeScript will infer the unexpected type if you give a null
value. In this case, you can specify the type explicitly:
constant<string | null>("MY_VALUE", null);
Value/refValue
The Value decorator or refValue function is used to inject a mutable value into a provider.
import {Injectable} from "@tsed/di";
@Injectable()
export class MyClass {
@Value("path.to.value")
private myValue: string;
constructor() {
console.log(this.myValue);
}
}
// server.ts
import {Configuration} from "@tsed/di";
@Configuration({
path: {to: {value: "myValue"}}
})
class Server {
}
import {refValue, injectable} from "@tsed/di";
export class MyClass {
private myValue = refValue<string>("path.to.value");
constructor() {
console.log(this.myValue.value);
this.myValue.value = "newValue";
}
}
injectable(MyClass);
// server.ts
import {configuration} from "@tsed/di";
class Server {
}
configuration(Server, {
path: {to: {value: "myValue"}}
})
Note that, value function can be used anywhere in your code, not only in a DI context:
function getMyValue(): string {
const value = value<string>("MY_VALUE");
console.log(value); // "myValue"
// do something
return value;
}
// or
const current = getMyValue();
console.log(current.value);
// or
@Injectable()
class MyService {
constructor() {
const current = getMyValue();
console.log(current.value); // "myValue"
}
}
Custom providers
The Ts.ED IoC resolves relationships providers for you, but sometimes, you want to tell to the DI how you want to instantiate a specific service or inject different kind of providers based on values, on asynchronous or synchronous factory or on external library. Look here to find more examples.
Configurable provider
Sometimes you need to inject a provider with a specific configuration to another one.
This is possible with the combination of Opts and UseOpts decorators.
import {Injectable, Opts, UseOpts} from "@tsed/di";
@Injectable()
class MyConfigurableService {
source: string;
constructor(@Opts options: any = {}) {
console.log("Hello ", options.source); // log: Hello Service1 then Hello Service2
this.source = options.source;
}
}
@Injectable()
class MyService1 {
constructor(@UseOpts({source: "Service1"}) service: MyConfigurableService) {
console.log(service.source); // log: Service1
}
}
@Injectable()
class MyService2 {
constructor(@UseOpts({source: "Service2"}) service: MyConfigurableService) {
console.log(service.source); // log: Service2
}
}
WARNING
Using Opts decorator on a constructor parameter changes the scope of the provider to ProviderScope.INSTANCE
.
Inject many providers
This feature simplifies dependency management when working with multiple implementations of the same interface using type code.
Using a token, you can configure injectable classe to be resolved as an array of instances using type
option:
import {Injectable} from "@tsed/di";
export interface Bar {
type: string;
}
export const Bar: unique symbol = Symbol("Bar");
@Injectable({type: Bar})
class Foo implements Bar {
private readonly name = "foo";
}
@Injectable({type: Bar})
class Baz implements Bar {
private readonly name = "baz";
}
import {injectable} from "@tsed/di";
export interface Bar {
type: string;
}
export const Bar: unique symbol = Symbol("Bar");
class Foo implements Bar {
private readonly name = "foo";
}
class Baz implements Bar {
private readonly type = "baz";
}
injectable(Foo).type(Bar);
injectable(Baz).type(Bar);
Now, we can use the Bar
token to inject all instances of Bar
identified by his type
:
import {Controller, Inject} from "@tsed/di";
import {Post} from "@tsed/schema";
import {BodyParams} from "@tsed/platform-params";
@Controller("/some")
export class SomeController {
constructor(@Inject(Bar) private readonly bars: Bar[]) {}
@Post()
async create(@BodyParams("type") type: "baz" | "foo") {
const bar: Bar | undefined = this.bars.find((x) => x.type === type);
}
}
import {controller, injectMany} from "@tsed/di";
import {BodyParams} from "@tsed/platform-params";
import {Post} from "@tsed/schema";
import {Bar} from "./Bar.js"
export class SomeController {
private readonly bars = injectMany<Bar>(Bar);
@Post()
async create(@BodyParams("type") type: "baz" | "foo") {
const bar: Bar | undefined = this.bars.find((x) => x.type === type);
}
}
controller(SomeController).path("/");
Alternatively, you can do this:
import {Controller, injectMany} from "@tsed/di";
import {Post} from "@tsed/schema";
import {BodyParams} from "@tsed/platform-params";
@Controller("/some")
export class SomeController {
@Post()
async create(@BodyParams("type") type: "baz" | "foo") {
const bar = injectMany<Bar>(Bar).find((x) => x.type === type);
}
}
AutoInjectable 7.82.0+
The AutoInjectable decorator let you create a class using new
that will automatically inject all dependencies from his constructor signature.
import {AutoInjectable} from "@tsed/di";
import {MyOtherService} from "./MyOtherService.js";
@AutoInjectable()
class MyService {
constructor(
opts: MyOptions,
private myOtherService?: MyOtherService
) {
console.log(myOtherService);
console.log(opts);
}
}
const myService = new MyService({
prop: "value"
});
In this example, we can see that MyService
is created using new
. We can give some options to the constructor and the rest of the dependencies will be injected automatically.
WARNING
AutoInjectable decorator only handles dependency injection when using new
. It doesn't register the class as a provider in the DI container. If you need the class to be available for injection in other classes, you must still use Injectable.
Interface abstraction
In some cases, you may want to use an interface to abstract the implementation of a service. This is a common pattern in TypeScript and can be achieved by using the provide
option in the @Injectable
decorator or the injectable().class()
function.
import {Injectable} from "@tsed/di";
export interface RetryPolicy {
retry<T extends (...args: unknown[]) => unknown>(task: T): Promise<ReturnType<T>>;
}
export const RetryPolicy: unique symbol = Symbol("RetryPolicy");
@Injectable({token: RetryPolicy})
export class TokenBucket implements RetryPolicy {
public retry<T extends (...args: unknown[]) => unknown>(task: T): Promise<ReturnType<T>> {
// ...
}
}
export interface RetryPolicy {
retry<T extends (...args: unknown[]) => unknown>(task: T): Promise<ReturnType<T>>;
}
export const RetryPolicy: unique symbol = Symbol("RetryPolicy");
class TokenBucket implements RetryPolicy {
public retry<T extends (...args: unknown[]) => unknown>(task: T): Promise<ReturnType<T>> {
// ...
}
}
injectable(RetryPolicy).class(TokenBucket);
Usage:
import {Inject, Injectable} from "@tsed/di";
import {RetryPolicy} from "./RetryPolicy.js";
@Injectable()
export class MyService {
constructor(@Inject(RetryPolicy) private readonly retryPolicy: RetryPolicy) {
// RetryPolicy will be automatically injected with its implementation (TokenBucket)
}
}
import {inject, injectable} from "@tsed/di";
import {RetryPolicy} from "./RetryPolicy.js";
export class MyService {
private readonly retryPolicy = inject(RetryPolicy);
}
injectable(MyService);
Define provider by environment 7.74.0+
Sometimes you need to import a provider depending on the environment or depending on a runtime context.
This is possible using the DI configuration imports
option that let you fine-tune the provider registration.
Here is an example of how to import a provider from a configuration:
import {Configuration} from "@tsed/di";
const TimeslotsRepository = Symbol.for("TimeslotsRepository");
interface TimeslotsRepository {
findTimeslots(): Promise<any[]>;
}
class DevTimeslotsRepository implements TimeslotsRepository {
findTimeslots(): Promise<any[]> {
return ["hello dev"];
}
}
class ProdTimeslotsRepository implements TimeslotsRepository {
findTimeslots(): Promise<any[]> {
return ["hello prod"];
}
}
@Configuration({
imports: [
{
token: TimeslotsRepository,
useClass: process.env.NODE_ENV === "production" ? ProdTimeslotsRepository : DevTimeslotsRepository
}
]
})
export class Server {}
You can also use injectable function to define your provider by environment:
import { injectable } from "@tsed/di";
import { Env } from "@tsed/core";
export interface TimeslotsRepository {
findTimeslots(): Promise<any[]>;
}
class DevTimeslotsRepository implements TimeslotsRepository {
findTimeslots(): Promise<any[]> {
return ["hello dev"];
}
}
class ProdTimeslotsRepository implements TimeslotsRepository {
findTimeslots(): Promise<any[]> {
return ["hello prod"];
}
}
export const TimeslotsRepository = injectable(Symbol.for("TimeslotsRepository"))
.class(process.env.NODE_ENV === Env.PROD ? ProdTimeslotsRepository : DevTimeslotsRepository)
.token();
Lazy load provider
By default, modules are eagerly loaded, which means that as soon as the application loads, so do all the modules, whether or not they are immediately necessary. While this is fine for most applications, it may become a bottleneck for apps running in the serverless environment, where the startup latency ("cold start")
is crucial.
Lazy loading can help decrease bootstrap time by loading only modules required by the specific serverless function invocation. Additionally, you can load other modules asynchronously once the serverless function is "warm" to speed up the bootstrap time for subsequent calls (deferred module registration).
You can read more about these techniques here.
Override provider
Any provider (Provider, Service, Controller, Middleware, etc...) already registered by Ts.ED or third-party can be overridden by your own class.
To override an existing provider, you can reuse the token to register your own provider using Injectable, OverrideProvider decorators or injectable function:
import {Injectable} from "@tsed/di";
import {PlatformCache} from "@tsed/platform-cache";
@Injectable({token: PlatformCache})
export class CustomCache extends PlatformCache {
/// do something
}
// server.ts
import "./services/CustomCache.js";
@Configuration({})
export class Server {
}
import {OriginalService} from "@tsed/platform-http";
import {OverrideProvider} from "@tsed/di";
@OverrideProvider(OriginalService)
export class CustomMiddleware extends OriginalService {
public method() {
// Do something
return super.method();
}
}
import {injectable} from "@tsed/di";
import {PlatformCache} from "@tsed/platform-cache";
class CustomCache extends PlatformCache {
/// do something
}
injectable(PlatformCache).class(CustomCache);
// server.ts
import "./services/CustomCache.js";
export class Server {
}
configuration(Server, {})
Just don't forget to import your provider in your project !
Inject context
The Context decorator or context function is used to inject the request context into a class or another function.
Context is a special object that contains all the information about the current request. It can be available in any injectable context, including controllers, services, and interceptors, while the request is being processed.
Here is an example to get context:
import {Injectable, Controller, InjectContext} from "@tsed/di";
import {PlatformContext} from "@tsed/platform-http";
@Injectable()
export class CustomRepository {
@InjectContext()
protected $ctx?: PlatformContext;
async findById(id: string) {
this.ctx?.logger.info("Where are in the repository");
return {
id,
headers: this.$ctx?.request.headers
};
}
}
@Controller("/async-hooks")
export class AsyncHookCtrl {
@Inject()
private readonly repository: CustomRepository;
@Get("/:id")
async get(@PathParams("id") id: string) {
return this.repository.findById(id);
}
}
import {context, controller, injectable} from "@tsed/di";
import {PlatformContext} from "@tsed/platform-http";
import {PathParams} from "@tsed/platform-params";
import {Get} from "@tsed/schema";
export class CustomRepository {
async findById(id: string) {
const $ctx = context<PlatformContext>();
$ctx?.logger.info("Where are in the repository");
return {
id,
headers: $ctx?.request.headers
};
}
}
injectable(CustomRepository);
export class AsyncHookCtrl {
private readonly repository = inject(CustomRepository);
@Get("/:id")
async get(@PathParams("id") id: string) {
return this.repository.findById(id);
}
}
controller(AsyncHookCtrl).path("/async-hooks");
See more about the context here.
Get injector
v8 allows you to get the injector instance everywhere in your code:
import {injector} from "@tsed/di";
function doSomething() {
const myService = injector().get<MyService>(MyService);
// shortcut to inject(MyService)
return myService.doSomething();
}