When I first started building B2B applications, I underestimated the complexity of the ‘SaaS’ part of the backend. It’s not just about creating CRUD endpoints; it’s about handling tenant isolation, subscription tiers, and scalable architecture. If you’re wondering how to build a SaaS backend with NestJS, you’ve chosen the right tool. NestJS provides the modularity and TypeScript first-class support that makes managing complex business logic significantly easier.
In my experience, the biggest mistake developers make is treating a SaaS backend like a standard app. You need a strategy for data isolation from day one. Whether you’re deciding on the best backend for startups or scaling an existing one, the foundation is everything.
Prerequisites
- Node.js (v18+) installed on your machine.
- Basic knowledge of TypeScript and Decorators.
- A PostgreSQL database instance (local or hosted via Supabase/RDS).
- NestJS CLI installed globally:
npm i -g @nestjs/cli.
Step 1: Architecting for Multi-Tenancy
The core of any SaaS is multi-tenancy. You have two main paths: Database-per-tenant (maximum isolation) or Shared database with tenant IDs (easier to manage). For most startups, the shared schema approach is the most pragmatic.
To implement this, I recommend using a Request-scoped provider to extract the tenantId from the header of every incoming request. As shown in the architecture diagram below, this ensures that your services always know which customer’s data they are accessing.
// tenant.middleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
@Injectable()
export class TenantMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
const tenantId = req.headers['x-tenant-id'];
if (!tenantId) {
throw new BadRequestException('Tenant ID is missing');
}
req['tenantId'] = tenantId;
next();
}
}
For a deeper dive into the nuances of isolation, check out my NestJS multi-tenancy guide where I compare these strategies in detail.
Step 2: Implementing Secure Authentication
For SaaS, you need more than just a login. You need Role-Based Access Control (RBAC) to differentiate between a ‘Tenant Admin’ and a ‘Standard User’. I’ve found that combining Passport.js with JWTs is the gold standard here.
Create a RolesGuard to protect specific endpoints based on the user’s subscription tier or organization role:
// roles.guard.ts
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.get<string[]>('roles', context.getHandler());
if (!requiredRoles) return true;
const { user } = context.switchToHttp().getRequest();
return requiredRoles.some((role) => user.roles?.includes(role));
}
}
Step 3: Integrating Subscription Management
Your backend needs to communicate with a payment provider to enable or disable features. I always recommend Stripe for its robust webhooks and billing portal. You don’t want to build your own billing engine—it’s a nightmare to maintain.
The flow is simple: User chooses plan → Stripe Checkout → Stripe Webhook → NestJS updates Tenant status in DB. You can find a detailed implementation in my Stripe integration for SaaS backends tutorial.
// payments.service.ts
@Injectable()
export class PaymentsService {
async handleWebhook(signature: string, payload: Buffer) {
const event = this.stripe.webhooks.constructEvent(payload, signature, process.env.STRIPE_WEBHOOK_SECRET);
if (event.type === 'customer.subscription.updated') {
const subscription = event.data.object;
await this.tenantService.updatePlan(subscription.customer, subscription.plan.id);
}
}
}
Pro Tips for Scaling Your SaaS Backend
- Use Interceptors for Logging: Implement a global interceptor to log every request along with its
tenantId. This makes debugging production issues 10x faster. - Database Indexing: In a shared schema, almost every query will have a
WHERE tenant_id = Xclause. Create composite indexes on(tenant_id, id)to keep performance snappy. - Avoid Circular Dependencies: As your SaaS grows, you’ll have many modules. Use
forwardRef()sparingly; instead, try to extract shared logic into aCoreModule.
Troubleshooting Common Issues
Issue: “Tenant ID is leaking between requests.”
Solution: Ensure your Tenant provider is SCOPE.REQUEST. If you use a singleton service to store the tenant ID, it will be shared across all users, leading to catastrophic data leaks.
Issue: “Stripe webhooks aren’t hitting my local server.”
Solution: Use the Stripe CLI to forward webhooks: stripe listen --forward-to localhost:3000/webhooks. I spent three hours debugging this before realizing I forgot the CLI!
What’s Next?
Now that you have the core backend running, you should focus on observability. I recommend integrating Prometheus and Grafana to monitor your API latency per tenant. This allows you to identify “noisy neighbors”—single tenants who are consuming an unfair share of your resources.