DI and decorators example
Try it
Start your own copy of this example:
npm create @setu-ts my-app -- --example di-decorators
cd my-app && npm install
npm start
The commands further down run inside a clone of the repository.
This app combines DiPlugin with DecoratorPlugin, with each feature declared as a @Module. The
decorated controller receives its GreetingService through the class-level
@Inject(GREETING_SERVICE).
LifetimesController (GET /lifetimes) visibly calls container.createScope(): singleton services
are shared across those scopes, while scoped services are retained inside one scope and differ
between scopes. The framework does not create a DI scope per request; applications that need
request scoping must create and carry that scope themselves.
pnpm --dir apps/di-decorators run start
pnpm --dir apps/di-decorators run smoke
Read the example deep dive for the concept and source
walkthrough.
Read, in order
src/modules/greeting/greeting.controller.ts
import { Controller, Get, Inject } from '@setu-ts/decorator-plugin';
import { GREETING_SERVICE } from './greeting.service.ts';
import type { GreetingService } from './greeting.service.ts';
/** A decorated controller; `@Inject` lists its constructor dependencies by position. */
@Controller('/greetings')
@Inject(GREETING_SERVICE)
export class GreetingController {
constructor(private readonly greetings: GreetingService) {}
@Get('/')
index(): { readonly greeting: string } {
return { greeting: this.greetings.greet('decorators') };
}
}
src/modules/greeting/greeting.module.ts
import { Module } from '@setu-ts/decorator-plugin';
import { GreetingController } from './greeting.controller.ts';
import { GreetingService } from './greeting.service.ts';
@Module({ controllers: [GreetingController], providers: [GreetingService] })
export class GreetingModule {}
src/modules/greeting/greeting.service.ts
import { Injectable } from '@setu-ts/decorator-plugin';
/** The DI token `GreetingService` is registered under. */
export const GREETING_SERVICE = 'greeting-service';
/** Builds greetings for the controller that injects it. */
@Injectable({ token: GREETING_SERVICE })
export class GreetingService {
greet(name: string): string {
return `Hello, ${name}!`;
}
}
src/modules/lifetimes/lifetimes.controller.ts
import { CAPABILITIES } from '@setu-ts/common';
import type { IContainer, IRequestContext } from '@setu-ts/common';
import { Controller, Ctx, Get, Inject, Params, Service } from '@setu-ts/decorator-plugin';
import { ScopedReportService, SingletonReportService } from './lifetimes.services.ts';
/**
* Per-request identity and lifetime comparisons.
* @since 0.11.0
*/
export interface LifetimesReport {
readonly singletonShared: boolean;
readonly scopeRetainsInstance: boolean;
readonly scopedId: string;
}
/**
* Demonstrates lazy request scopes through Service and forRequest.
* @since 0.11.0
*/
@Controller('/lifetimes')
@Inject(CAPABILITIES.DI_CONTAINER)
export class LifetimesController {
/**
* @param container - Application container
* @since 0.11.0
*/
constructor(private readonly container: IContainer) {}
/**
* Compares parameter-injected services with the same request scope.
*
* @param scoped - Request-owned service
*
* @param singleton - Application-owned service
*
* @param ctx - Context owning the scope and its disposal
*
* @returns Instance identity comparisons and this request service identifier
*
* @since 0.11.0
*/
@Get('/')
@Params(Service(ScopedReportService), Service(SingletonReportService), Ctx())
report(
scoped: ScopedReportService,
singleton: SingletonReportService,
ctx: IRequestContext,
): LifetimesReport {
const scope = this.container.forRequest(ctx);
return {
singletonShared: singleton === scope.resolve(SingletonReportService),
scopeRetainsInstance: scoped === scope.resolve(ScopedReportService),
scopedId: scoped.id,
};
}
}
src/modules/lifetimes/lifetimes.module.ts
import { Module } from '@setu-ts/decorator-plugin';
import { LifetimesController } from './lifetimes.controller.ts';
import { ScopedReportService, SingletonReportService } from './lifetimes.services.ts';
/**
* Demonstrates singleton and lazily created HTTP request lifetimes.
* @since 0.11.0
*/
@Module({
controllers: [LifetimesController],
providers: [SingletonReportService, ScopedReportService],
})
export class LifetimesModule {}
src/modules/lifetimes/lifetimes.services.ts
import type { IRuntimeServices } from '@setu-ts/common';
import { CAPABILITIES } from '@setu-ts/common';
import { Inject, Injectable } from '@setu-ts/decorator-plugin';
export const SINGLETON_REPORT = 'singleton-report';
export const SCOPED_REPORT = 'scoped-report';
/** One instance for the whole application. */
@Injectable({ scope: 'singleton', token: SINGLETON_REPORT })
export class SingletonReportService {}
/**
* One instance per lazy request scope.
* @since 0.11.0
*/
@Injectable({ scope: 'scoped', token: SCOPED_REPORT })
@Inject(CAPABILITIES.RUNTIME)
export class ScopedReportService {
/**
* Request service identity.
* @since 0.11.0
*/
readonly id: string;
/**
* @param runtime - Portable identifier source
* @since 0.11.0
*/
constructor(runtime: IRuntimeServices) {
this.id = runtime.uuid();
}
}
smoke.ts
import assert from 'node:assert/strict';
import { after, before, describe, it } from 'node:test';
import type { IKernelApplication } from '@setu-ts/kernel';
import type { LifetimesReport } from './src/modules/lifetimes/lifetimes.controller.ts';
import { createApp } from './setu.config.ts';
describe('di-decorators example', () => {
let app: IKernelApplication;
before(async () => {
app = createApp();
await app.start();
});
after(async () => {
await app.stop();
});
it('answers through the service injected into the controller', async () => {
const response = await app.inject({ method: 'GET', url: '/greetings/' });
assert.equal(response.statusCode, 200);
assert.deepEqual(response.json(), { greeting: 'Hello, decorators!' });
});
it('shares singletons and creates a distinct scoped service for each request', async () => {
const first = await app.inject({ method: 'GET', url: '/lifetimes/' });
const second = await app.inject({ method: 'GET', url: '/lifetimes/' });
assert.equal(first.statusCode, 200);
assert.equal(second.statusCode, 200);
const a = first.json<LifetimesReport>();
const b = second.json<LifetimesReport>();
assert.equal(a.singletonShared, true);
assert.equal(b.singletonShared, true);
assert.equal(a.scopeRetainsInstance, true);
assert.equal(b.scopeRetainsInstance, true);
assert.notEqual(a.scopedId, b.scopedId);
});
});