technology
Designing Scalable and Efficient Multi-Tenant SaaS Architectures
Designing Scalable and Efficient Multi-Tenant SaaS Architectures with NestJS, Prisma, and TypeScript
TL;DR
In today's rapidly evolving tech landscape, developing scalable and efficient multi-tenant SaaS (Software as a Service) architectures is crucial. Multi-tenancy allows a single instance of your software to serve multiple customers, or "tenants," while ensuring data isolation and security. This article will guide you through building a robust multi-tenant SaaS architecture using NestJS, Prisma, and TypeScript.
Why Multi-Tenancy?
Multi-tenancy offers several advantages:
Cost Efficiency: Shared resources reduce operational costs.
Scalability: Easily accommodate more tenants without significant architectural changes.
Simplified Maintenance: Centralized updates and maintenance.
The Tech Stack
NestJS
NestJS is a progressive Node.js framework for building efficient and scalable server-side applications. It uses TypeScript and leverages robust architectural patterns, making it ideal for complex applications.
Prisma
Prisma is an open-source ORM (Object-Relational Mapping) tool that simplifies database access with a type-safe query builder. It supports multi-tenancy, making it a perfect fit for our use case.
TypeScript
TypeScript provides static typing, which enhances code quality and maintainability, crucial for building large-scale applications.
Real-Life Product Idea: Multi-Tenant Project Management Tool
Let's imagine we're building a multi-tenant project management tool, "TaskMaster," which allows multiple organizations to manage their projects, tasks, and teams within a single application instance.
Designing the Architecture
1. Setting Up the Project
First, set up a new NestJS project and install Prisma:
bash
Copy code
npm i -g @nestjs/cli
nest new taskmaster
cd taskmaster
npm install prisma @prisma/client
2. Configuring Prisma
Initialize Prisma and create your schema:
bash
Copy code
npx prisma init
Edit the prisma/schema.prisma
file:
prisma
Copy code
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
model Tenant {
id Int @id @default(autoincrement())
name String
users User[]
projects Project[]
}
model User {
id Int @id @default(autoincrement())
email String @unique
password String
tenantId Int
tenant Tenant @relation(fields: [tenantId], references: [id])
tasks Task[]
}
model Project {
id Int @id @default(autoincrement())
name String
tenantId Int
tenant Tenant @relation(fields: [tenantId], references: [id])
tasks Task[]
}
model Task {
id Int @id @default(autoincrement())
title String
completed Boolean @default(false)
projectId Int
project Project @relation(fields: [projectId], references: [id])
userId Int
user User @relation(fields: [userId], references: [id])
}
Run Prisma migrate to create the database schema:
bash
Copy code
npx prisma migrate dev --name init
3. Building the NestJS Application
Creating Modules and Services
Generate modules and services for tenants, users, projects, and tasks:
bash
Copy code
nest g module tenants
nest g service tenants
nest g controller tenants
nest g module users
nest g service users
nest g controller users
nest g module projects
nest g service projects
nest g controller projects
nest g module tasks
nest g service tasks
nest g controller tasks
Implementing Tenant Service
In tenants/tenants.service.ts
:
typescript
Copy code
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma.service';
@Injectable()
export class TenantsService {
constructor(private prisma: PrismaService) {}
async createTenant(data: { name: string }) {
return this.prisma.tenant.create({
data,
});
}
async getTenants() {
return this.prisma.tenant.findMany();
}
}
4. Middleware for Multi-Tenancy
Implement a middleware to determine the current tenant based on the request:
typescript
Copy code
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 Error('No tenant ID provided');
}
req['tenantId'] = tenantId;
next();
}
}
Apply this middleware in main.ts
:
typescript
Copy code
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { TenantMiddleware } from './tenant.middleware';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.use(new TenantMiddleware().use);
await app.listen(3000);
}
bootstrap();
5. Integrating Prisma with Multi-Tenancy
Modify Prisma service to use tenant context:
typescript
Copy code
import { Injectable, Scope } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
@Injectable({ scope: Scope.REQUEST })
export class PrismaService extends PrismaClient {
constructor() {
super();
}
setTenantId(tenantId: number) {
this.tenantId = tenantId;
}
}
Update the tenant-related services to use the tenant context:
typescript
Copy code
import { Injectable, Inject, Scope } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { Request } from 'express';
import { PrismaService } from '../prisma.service';
@Injectable({ scope: Scope.REQUEST })
export class UsersService {
constructor(
private prisma: PrismaService,
@Inject(REQUEST) private request: Request,
) {
const tenantId = Number(this.request['tenantId']);
this.prisma.setTenantId(tenantId);
}
async createUser(data: { email: string; password: string }) {
return this.prisma.user.create({
data: {
...data,
tenantId: this.prisma.tenantId,
},
});
}
async getUsers() {
return this.prisma.user.findMany({
where: { tenantId: this.prisma.tenantId },
});
}
}
Conclusion
By leveraging NestJS, Prisma, and TypeScript, you can build a scalable and efficient multi-tenant SaaS application. This architecture ensures data isolation, security, and scalability. Whether you're building a project management tool like "TaskMaster" or any other SaaS application, this approach will help you achieve a robust and maintainable solution.
Embrace these technologies and design patterns to stay ahead in the competitive SaaS market. Happy coding!