export abstract class PaymentGateway {
abstract charge(amount: Money): Promise<ChargeId>;
}
@Injectable()
export class OrdersService {
constructor(private readonly payments: PaymentGateway) {}
}
// orders.module.ts
providers: [OrdersService, { provide: PaymentGateway, useClass: StripeGateway }]
The service should depend on what it needs (charge a payment), not on which company does it.
Why:
- Swap without edits: switching to another provider, or a sandbox in staging, changes one line in the module.
- Easy tests: pass a fake gateway. No Stripe calls, and no network.
- Clear boundary: Stripe's SDK types stay inside
StripeGateway.
Why an abstract class works as the token: Nest reads constructor parameter types from metadata that TypeScript emits. An interface is erased, so it would need @Inject("PAYMENT_GATEWAY"). An abstract class is a real value at runtime. So { provide: PaymentGateway, useClass: StripeGateway } works with no @Inject. StripeGateway can use implements PaymentGateway, so it doesn't inherit anything.
Parameter properties: private readonly payments: PaymentGateway in the constructor declares the field and assigns it in one step.
Note: a NestJS provider is a singleton by default. So the class must not keep per-request data in fields.
- Depending on an abstraction lets you swap the implementation in one place.
- Tests can pass a fake implementation with no network calls.
- Interfaces are erased, so they need a string or symbol token with
@Inject. - An abstract class exists at runtime, so it works as a token without
@Inject. - Constructor parameter properties declare and assign the field in one step.