Raghav

technology

Designing Scalable and Efficient Multi-Tenant SaaS Architectures

Designing Scalable and Efficient Multi-Tenant SaaS Architectures with NestJS, Prisma, and TypeScript

pp for raghav
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!