| name | nestjs-backend-patterns |
| description | NestJS backend architecture patterns for multi-tenant SaaS applications. Use when integrating third-party services/APIs into NestJS, setting up Prisma or ZenStack, implementing multitenancy patterns, building authentication/authorization, creating provider patterns for external services, applying access control with RBAC or policies, or handling database connection and repository patterns. |
NestJS Backend Architecture Patterns
Patterns for building scalable, maintainable NestJS backends in an Nx monorepo.
Activation scope
This skill activates per-module during Stage B of the SaaS bootstrap. saas-workspace-initializer may scaffold thin tenant/auth primitives in the Stage A foundation, but full provider-pattern integrations, multitenancy enforcement, and Prisma/ZenStack policy work happen in dedicated Stage B sessions driven by individual roadmap items. Pull this skill in when implementing one such item, not to retrofit an entire backend.
Core Principle: Provider Pattern for External Services
All third-party integrations follow the same provider pattern:
export const ThirdPartyClientProvider = {
provide: 'THIRD_PARTY_CLIENT',
useFactory: (configService: ConfigService) => {
return new ThirdPartySDK(configService.get('THIRD_PARTY_API_KEY'));
},
inject: [ConfigService],
};
@Injectable()
export class ThirdPartyService {
constructor(@Inject('THIRD_PARTY_CLIENT') private client: ThirdPartySDK) {}
async doSomething() {
return this.client.operation();
}
}
@Module({
imports: [ConfigModule],
providers: [ThirdPartyClientProvider, ThirdPartyService],
exports: [ThirdPartyService],
})
export class ThirdPartyModule {}
Quick Reference
Third-Party Integration Pattern
Step 1: Provider Factory
import { ConfigService } from '@nestjs/config';
export const PaymentClientProvider = {
provide: 'PAYMENT_CLIENT',
useFactory: (config: ConfigService) => {
const provider = config.get('PAYMENT_PROVIDER');
const apiKey = config.get('PAYMENT_API_KEY');
switch (provider) {
case 'stripe':
return new StripeClient(apiKey);
case 'paddle':
return new PaddleClient(apiKey);
default:
throw new Error(`Unknown payment provider: ${provider}`);
}
},
inject: [ConfigService],
};
Step 2: Abstract Service Interface
export interface PaymentClient {
createCustomer(data: CreateCustomerDto): Promise<Customer>;
createSubscription(data: CreateSubscriptionDto): Promise<Subscription>;
cancelSubscription(id: string): Promise<void>;
handleWebhook(payload: Buffer, signature: string): Promise<WebhookEvent>;
}
Step 3: Service Implementation
@Injectable()
export class PaymentService {
constructor(@Inject('PAYMENT_CLIENT') private client: PaymentClient) {}
async createCustomer(dto: CreateCustomerDto): Promise<Customer> {
return this.client.createCustomer(dto);
}
async createSubscription(dto: CreateSubscriptionDto): Promise<Subscription> {
return this.client.createSubscription(dto);
}
}
Multitenancy Pattern (Shared Schema)
@Injectable()
export class TenantAwarePrismaService {
constructor(
private prisma: PrismaService,
private cls: ClsService,
) {}
get client() {
const tenantId = this.cls.get('tenantId');
if (!tenantId) {
throw new UnauthorizedException('No tenant context');
}
return enhance(this.prisma, { user: { tenantId } });
}
}
Module Organization
libs/
├── shared/
│ ├── prisma-client/ # Prisma + ZenStack setup
│ │ ├── prisma.service.ts
│ │ ├── enhanced-prisma.service.ts
│ │ └── prisma-client.module.ts
│ ├── auth/ # Authentication infrastructure
│ │ ├── strategies/
│ │ │ └── jwt.strategy.ts
│ │ ├── guards/
│ │ │ ├── jwt-auth.guard.ts
│ │ │ └── roles.guard.ts
│ │ └── auth.module.ts
│ └── [service-name]/ # Third-party integrations
│ ├── [name]-client.provider.ts
│ ├── [name].service.ts
│ └── [name].module.ts
└── [domain]/
└── feature-api/
├── controllers/
├── dto/
└── [domain].module.ts
Decision Matrix
References
Load these for detailed implementation guidance: