Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
40 prioritized NestJS best practices across architecture, DI, security, performance, testing, and microservices.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
rules/di-liskov-substitution.md
1---2title: Honor Liskov Substitution Principle3impact: HIGH4impactDescription: Ensures implementations are truly interchangeable without breaking callers5tags: dependency-injection, inheritance, solid, lsp6---78## Honor Liskov Substitution Principle910Subtypes must be substitutable for their base types without altering program correctness. In NestJS with dependency injection, this means any implementation of an interface or abstract class must honor the contract completely. A mock payment service used in tests must behave like a real payment service (return similar shapes, handle errors the same way). Violating LSP causes subtle bugs when swapping implementations.1112**Incorrect (implementation violates the contract):**1314```typescript15// Base interface with clear contract16interface PaymentGateway {17/**18* Charges the specified amount.19* @returns PaymentResult on success20* @throws PaymentFailedException on payment failure21*/22charge(amount: number, currency: string): Promise<PaymentResult>;23}2425// Production implementation - follows the contract26@Injectable()27export class StripeService implements PaymentGateway {28async charge(amount: number, currency: string): Promise<PaymentResult> {29const response = await this.stripe.charges.create({ amount, currency });30return { success: true, transactionId: response.id, amount };31}32}3334// Mock that violates LSP - different behavior!35@Injectable()36export class MockPaymentService implements PaymentGateway {37async charge(amount: number, currency: string): Promise<PaymentResult> {38// VIOLATION 1: Throws for valid input (contract says return PaymentResult)39if (amount > 1000) {40throw new Error('Mock does not support large amounts');41}4243// VIOLATION 2: Returns null instead of PaymentResult44if (currency !== 'USD') {45return null as any; // Real service would convert or reject properly46}4748// VIOLATION 3: Missing required field49return { success: true } as PaymentResult; // Missing transactionId!50}51}5253// Consumer trusts the contract54@Injectable()55export class OrdersService {56constructor(@Inject(PAYMENT_GATEWAY) private payment: PaymentGateway) {}5758async checkout(order: Order): Promise<void> {59const result = await this.payment.charge(order.total, order.currency);60// These fail with MockPaymentService:61await this.saveTransaction(result.transactionId); // undefined!62await this.sendReceipt(result); // might be null!63}64}65```6667**Correct (implementations honor the contract):**6869```typescript70// Well-defined interface with documented behavior71interface PaymentGateway {72/**73* Charges the specified amount.74* @param amount - Amount in smallest currency unit (cents)75* @param currency - ISO 4217 currency code76* @returns PaymentResult with transactionId, success status, and amount77* @throws PaymentFailedException if charge is declined78* @throws InvalidCurrencyException if currency is not supported79*/80charge(amount: number, currency: string): Promise<PaymentResult>;8182/**83* Refunds a previous charge.84* @throws TransactionNotFoundException if transactionId is invalid85*/86refund(transactionId: string, amount?: number): Promise<RefundResult>;87}8889// Production implementation90@Injectable()91export class StripeService implements PaymentGateway {92async charge(amount: number, currency: string): Promise<PaymentResult> {93try {94const response = await this.stripe.charges.create({ amount, currency });95return {96success: true,97transactionId: response.id,98amount: response.amount,99};100} catch (error) {101if (error.type === 'card_error') {102throw new PaymentFailedException(error.message);103}104throw error;105}106}107108async refund(transactionId: string, amount?: number): Promise<RefundResult> {109// Implementation...110}111}112113// Mock that honors LSP - same contract, same behavior shape114@Injectable()115export class MockPaymentService implements PaymentGateway {116private transactions = new Map<string, PaymentResult>();117118async charge(amount: number, currency: string): Promise<PaymentResult> {119// Honor the contract: validate currency like real service would120if (!['USD', 'EUR', 'GBP'].includes(currency)) {121throw new InvalidCurrencyException(`Unsupported currency: ${currency}`);122}123124// Simulate decline for specific test scenarios125if (amount === 99999) {126throw new PaymentFailedException('Card declined (test scenario)');127}128129// Return same shape as production130const result: PaymentResult = {131success: true,132transactionId: `mock_${Date.now()}_${Math.random().toString(36)}`,133amount,134};135136this.transactions.set(result.transactionId, result);137return result;138}139140async refund(transactionId: string, amount?: number): Promise<RefundResult> {141// Honor the contract: throw if transaction not found142if (!this.transactions.has(transactionId)) {143throw new TransactionNotFoundException(transactionId);144}145146return {147success: true,148refundId: `refund_${transactionId}`,149amount: amount ?? this.transactions.get(transactionId)!.amount,150};151}152}153154// Consumer can swap implementations safely155@Injectable()156export class OrdersService {157constructor(@Inject(PAYMENT_GATEWAY) private payment: PaymentGateway) {}158159async checkout(order: Order): Promise<Order> {160try {161const result = await this.payment.charge(order.total, order.currency);162// Works with both StripeService and MockPaymentService163order.transactionId = result.transactionId;164order.status = 'paid';165return order;166} catch (error) {167if (error instanceof PaymentFailedException) {168order.status = 'payment_failed';169return order;170}171throw error;172}173}174}175```176177**Testing LSP compliance:**178179```typescript180// Shared test suite that any implementation must pass181function testPaymentGatewayContract(182createGateway: () => PaymentGateway,183) {184describe('PaymentGateway contract', () => {185let gateway: PaymentGateway;186187beforeEach(() => {188gateway = createGateway();189});190191it('returns PaymentResult with all required fields', async () => {192const result = await gateway.charge(1000, 'USD');193expect(result).toHaveProperty('success');194expect(result).toHaveProperty('transactionId');195expect(result).toHaveProperty('amount');196expect(typeof result.transactionId).toBe('string');197});198199it('throws InvalidCurrencyException for unsupported currency', async () => {200await expect(gateway.charge(1000, 'INVALID'))201.rejects.toThrow(InvalidCurrencyException);202});203204it('throws TransactionNotFoundException for invalid refund', async () => {205await expect(gateway.refund('nonexistent'))206.rejects.toThrow(TransactionNotFoundException);207});208});209}210211// Run against all implementations212describe('StripeService', () => {213testPaymentGatewayContract(() => new StripeService(mockStripeClient));214});215216describe('MockPaymentService', () => {217testPaymentGatewayContract(() => new MockPaymentService());218});219```220221Reference: [Liskov Substitution Principle](https://en.wikipedia.org/wiki/Liskov_substitution_principle)222