Constructor injection is the primary method of dependency injection in PBinJ. It provides a clean, type-safe way to declare and use dependencies.
The most basic form of constructor injection uses the pbj() function in constructor parameters:
import { pbj } from "@pbinj/pbj";
class UserService {
constructor(
private logger = pbj(LoggerService),
private database = pbj(DatabaseService),
) {}
}PBinJ provides full type safety for constructor injection. TypeScript will infer the correct types from your services:
import { pbj } from "@pbinj/pbj";
class LoggerService {
log(message: string): void {
console.log(message);
}
}
class UserService {
constructor(private logger = pbj(LoggerService)) {
// TypeScript knows logger has a log() method
this.logger.log("UserService initialized");
}
}Dependencies are optional by default. You can handle cases where a dependency might not be available:
import { pbj } from "@pbinj/pbj";
class AnalyticsService {
constructor(
private logger = pbj(LoggerService),
private metrics = pbj(MetricsService),
) {}
trackEvent(name: string) {
this.logger.log(`Tracking: ${name}`);
// Safely handle optional dependency
this.metrics?.record(name);
}
}To make a dependency required, you can use the service.withOptional(false) modifier:
@pbinj
import { context } from "@pbinj/pbj";
class LoggerService {
log(message: string): void {
console.log(message);
}
}
//This will throw an error if LoggerService is not registered
context.register(LoggerService).withOptional(false);You can inject factory functions that create instances with additional parameters:
import { pbj, context } from "@pbinj/pbj";
class ConfigService {
constructor(readonly dbConnectionString = "postgres://localhost:5432") {}
}
class DatabaseConnection {
constructor(private connectionString: string) {}
}
// Register with a factory
context.register(DatabaseConnection, (config = pbj(ConfigService)) => {
return new DatabaseConnection(config.dbConnectionString);
});
class UserRepository {
constructor(private db = pbj(DatabaseConnection)) {}
}Use pbjKey to manage multiple implementations of the same interface:
import { pbj, pbjKey } from "@pbinj/pbj";
interface Cache {
get(key: string): Promise<string | null>;
set(key: string, value: string): Promise<void>;
}
const memoryCache = pbjKey<Cache>("memory-cache");
const redisCache@pbinjy<Cache>("redis-cache");
declare module "@pbinj/pbj" {
interface Registry {
[memoryCache]: Cache;
[redisCache]: Cache;
}
}
class CacheService {
constructor(
private primary = pbj(redisCache),
private fallback = pbj(memoryCache)
) {}
async get(key: string): Promise<string | null> {
return (await this.primary.get(key)) ?? (await this.fallback.get(key));
}
}-
Interface Keys: Use
pbjKeyfor interfaces and abstract classes:interface ILogger { log(message: string): void; } const loggerKey = pbjKey<ILogger>("logger");
import { env, envRequired } from "@pbinj/pbj/env";
import { pbj } from "@pbinj/pbj";
class ConfigService {
constructor(
readonly apiUrl = env("API_URL", "http://localhost:3000"),
readonly apiKey = envRequired("API_KEY"),
) {}
}
class ApiClient {
constructor(private config = pbj(ConfigService)) {}
async request(path: string) {
return fetch(`${this.config.apiUrl}${path}`, {
headers: { Authorization: this.config.apiKey },
});
}
}import { pbj } from "@pbinj/pbj";
class UserController {
constructor(
private users = pbj(UserService),
private auth = pbj(AuthService),
) {}
async getUser(id: string) {
if (!this.auth.isAuthenticated()) {
throw new Error("Unauthorized");
}
return this.users.getUser(id);
}
}@pbinj
Constructor injection makes testing easier by allowing you to mock dependencies:
import { context } from "@pbinj/pbj";
import { describe, it } from "vitest";
describe("UserService", () => {
it("should create user", async () => {
// Mock dependencies
const mockLogger = { log: vi.fn() };
const mockDb = { saveUser: vi.fn() };
// Register mocks
context.register(loggerKey, mockLogger);
context.register(dbKey, mockDb);
// Test the service
const userService = context.resolve(UserService);
await userService.createUser({ name: "Test" });
expect(mockLogger.log).toHaveBeenCalled();
expect(mockDb.saveUser).toHaveBeenCalled();
});
});