Multi-Tenancy Made Simple in NestJS

A Beginner’s Guide to Shared Architecture

Introduction

In this post, I will elucidate the concept of multi-tenancy through a straightforward demonstration using Nest.js.

Prior familiarity with Typescript, JavaScript, basic understanding of Nest.js, REST API concepts, and dependency injection is assumed.

If you’re new to Nest.js, I highly recommend exploring its documentation as it’s an exceptional framework for constructing resilient web applications.

For those already acquainted with multi-tenancy in other technology stacks and seeking insights into its implementation with Nest.js, feel free to directly examine the code on my GitHub repository.

Let's get started

Firstly, install the Nest.js CLI program if you haven’t done so already.

Execute nest new multitenant to scaffold the project. I opted for pnpm as the package manager. Alternatively, you can select npm or yarn if you prefer not to use pnpm.

Let's see what is multi-tenancy

Multi-tenancy is a prevalent architectural approach employed in Software as a Service (SaaS) applications, facilitating resource sharing among customers, commonly referred to as tenants.

The most important concept in Multitenancy is Host vs Tenant

The host is responsible for owning and overseeing the management of the SaaS application’s system.

A tenant refers to a paying customer of the SaaS application who utilizes the service.

Let’s code

Firstly, we should create a generic IService interface which will be used in our application services later.

Create a file app.interface.ts inside src the folder.

export interface IService<T, C, U> {
  get: (uuid: string, tenantId?: string) => T;
  create: (data: C, tenantId?: string) => void;
  update: (uuid: string, data: U, tenantId?: string) => void;
  delete: (uuid: string, tenantId?: string) => void;
  getAll: (tenantId?: string) => T[];
}

In our application, T denotes the model entity, exemplified by TodoModel. C and U refer to CreateDto and UpdateDto respectively. Additionally, we have an optional tenantId property, which holds significance for reasons that will become apparent shortly.

Next, let’s create two folders, tenant and todo, within the src directory to organize our application’s folders based on features. The tenant feature serves as our primary focus, while todo functions as the service accessible to our tenants (customers), which the host can utilize as well.

With the basic setup in place, let’s proceed to complete the tenant feature. The tenant folder encompasses its logic, including models, controllers, services, middleware, etc.

Folder structure example

Setup Tenant Logic

Let’s begin by creating the DTOs, models, and services for the tenant feature. Within the models folder, initiate a file named TenantModel.ts.

export class TenantModel {
  id: string;
  name: string;
  subdomain?: string; // https://store.mysassapp.com
  constructor(id: string, name: string, subdomain?: string) {
    this.id = id;
    this.name = name;
    this.subdomain = subdomain || 'https://mysassapp.com';
  }
}

The tenant model comprises essential properties such as id and name, both of which are mandatory. Additionally, it can include a domain or subdomain, providing options for customers to customize their domain.

Note: I am not going to cover the domain/subdomain part in the post.

Let’s create dtos and services now.

src/tenant/dtos/CreateTenantDto.ts

export class CreateTenantDto {
  name: string;
  subdomain?: string;
  constructor(name: string, subdomain?: string) {
    this.name = name;
    this.subdomain = subdomain;
  }
}

src/tenant/dtos/UpdateTenantDto.ts

export class UpdateTenantDto {
  id: string;
  name: string;
  subdomain?: string;
  constructor(id: string, name: string, subdomain?: string) {
    this.id = id;
    this.name = name;
    this.subdomain = subdomain;
  }
}

src/tenant/services/TenantService.ts 

import { randomUUID } from 'crypto';
import { Injectable, NotFoundException } from '@nestjs/common';
import { IService } from '../../app.interface';
import { TenantModel } from '../models/TenantModel';
import { CreateTenantDto } from '../dtos/CreateTenantDto';
import { UpdateTenantDto } from '../dtos/UpdateTenantDto';

@Injectable()
export class TenantService
  implements IService<TenantModel, CreateTenantDto, UpdateTenantDto>
{
  private readonly tenants: TenantModel[] = []; // Temp local database.. 

  create(data: CreateTenantDto): void {
    const uuid = randomUUID();
    this.tenants.push(new TenantModel(uuid, data.name, data.subdomain));
  }

  delete(uuid: string): void {
    const index = this.tenants.findIndex((tenant) => tenant.id === uuid);
    if (index === -1) throw new NotFoundException('Tenant not found');
    this.tenants.splice(index, 1);
  }

  get(uuid: string): TenantModel {
    const todo = this.tenants.find((tenant) => tenant.id === uuid);
    if (!todo) throw new NotFoundException('Tenant not found');
    return todo;
  }

  update(uuid: string, data: UpdateTenantDto): void {
    const tenant = this.tenants.find((tenant) => tenant.id === uuid);
    if (!tenant) throw new NotFoundException('Tenant not found');
    tenant.name = data.name;
    tenant.subdomain = data.subdomain; 
  }

  getAll(): TenantModel[] {
    return this.tenants;
  }
}

In the tenant service, I’m currently utilizing a temporary array to store tenant information. In a production environment, it’s imperative to integrate a real database for data persistence. Furthermore, you have the option to configure different databases for individual tenants. (Feel free to inquire in the comments if you’re interested in learning how to implement this functionality.)

Moving forward let’s create our tenant controller.

src/tenant/controllers/tenant.controller.ts

import {
  Body,
  Controller,
  Delete,
  Get,
  HttpStatus,
  Param,
  Post,
  Put,
  Req,
} from '@nestjs/common';
import { CreateTenantDto } from '../dtos/CreateTenantDto';
import { UpdateTenantDto } from '../dtos/UpdateTenantDto';
import { TenantService } from '../services/TenantService';

@Controller()
export class TenantController {
  constructor(private readonly tenantService: TenantService) {}

  @Get('/tenants')
  getAll() {
    return this.tenantService.getAll();
  }

  @Post('/tenants')
  createTodo(@Req() req: Request, @Body() data: CreateTenantDto) {
    this.tenantService.create(data);
    return HttpStatus.CREATED;
  }

  @Get('/tenants/:uuid')
  getTenant(@Req() req: Request, @Param('uuid') uuid: string) {
    return this.tenantService.get(uuid);
  }

  @Put('/tenants/:uuid')
  updateTenant(
    @Req() req: Request,
    @Param('uuid') uuid: string,
    @Body() data: UpdateTenantDto,
  ) {
    this.tenantService.update(uuid, data);
    return HttpStatus.NO_CONTENT;
  }

  @Delete('/tenants/:uuid')
  deleteTodo(@Req() req: Request, @Param('uuid') uuid: string) {
    this.tenantService.delete(uuid);
    return HttpStatus.ACCEPTED;
  }
}

Alright, we have finished implementing the tenant logic. Next, register the controller and service in the app.module.ts. Afterwards, launch your preferred REST client, such as Postman, and test the endpoint.

Ok, the tenant API working as expected. Let’s create our todo logic.

Setup Todo Logic

Let’s continue creating the todo’s dtos, models and services first. Create a todo model inside the model's folder TodoModel.ts

export class TodoModel {
  uuid: string;
  title: string;
  done: boolean;
  tenantId?: string;

  constructor(uuid: string, title: string, done: boolean) {
    this.uuid = uuid;
    this.title = title;
    this.done = done;
  }

  setTenantId(tenantId: string) {
    this.tenantId = tenantId;
  }

In our todo model, the tenantId property is optional. This is designed to facilitate the management of ownership for each todo item. For example, a host may have their todo item, which remains invisible to any of the host’s customers (tenants).

Note: In the actual production code, you must possess a todo model entity that establishes a many-to-one relationship.

Let’s continue with the rest of the todo logic.

src/todo/dtos/CreateTodoDto.ts

export class CreateTodoDto {
  title: string;
  done: boolean;
  constructor(title: string, done: boolean) {
    this.title = title;
    this.done = done;
  }
}

src/todo/dtos/UpdateTodoDto.ts

export class UpdateTodoDto {
  id: string;
  title: string;
  done: boolean;
  constructor(id: string, title: string, done: boolean) {
    this.title = title;
    this.done = done;
  }
}

src/todo/services/TodoService.ts

import { randomUUID } from 'crypto';
import { Injectable, NotFoundException } from '@nestjs/common';
import { IService } from '../../app.interface';
import { TodoModel } from '../models/TodoModel';
import { CreateTodoDto } from '../dtos/CreateTodoDto';
import { UpdateTodoDto } from '../dtos/UpdateTodoDto';

@Injectable()
export class TodoService
  implements IService<TodoModel, CreateTodoDto, UpdateTodoDto>
{
  private readonly todos: TodoModel[] = []; // temp local databse to store all our todo items

  create(data: CreateTodoDto, tenantId?: string): void {
    const uuid = randomUUID();
    const newTodo = new TodoModel(uuid, data.title, data.done);
    if (tenantId) newTodo.setTenantId(tenantId);
    this.todos.push(newTodo);
  }

  delete(uuid: string, tenantId?: string) {
    const index = this.todos.findIndex((todo) => todo.uuid === uuid);
    if (index === -1) throw new NotFoundException('Todo not found');
    if (tenantId && this.todos[index].tenantId !== tenantId)
      throw new NotFoundException('Todo not found');
    this.todos.splice(index, 1);
  }

  get(uuid: string, tenantId?: string): TodoModel {
    const todo = this.todos.find((todo) => todo.uuid === uuid);
    if (!todo) throw new NotFoundException('Todo not found');
    if (tenantId && todo.tenantId !== tenantId)
      throw new NotFoundException('Todo not found');
    return todo;
  }

  update(uuid: string, data: UpdateTodoDto, tenantId?: string): TodoModel {
    const todo = this.todos.find((todo) => todo.uuid === uuid);
    if (!todo) throw new NotFoundException('Todo not found');
    if (tenantId && todo.tenantId !== tenantId)
      throw new NotFoundException('Todo not found');
    todo.title = data.title;
    todo.done = data.done;
    return todo;
  }

  getAll(tenantId?: string): TodoModel[] {
    if (tenantId)
      return this.todos.filter((todo) => todo.tenantId === tenantId);
    return this.todos.filter((todo) => !todo.tenantId);
  }
}

src/todo/controllers/todo.controller.ts

import {
  Body,
  Controller,
  Delete,
  Get,
  HttpStatus,
  Param,
  Post,
  Put,
  Req,
} from '@nestjs/common';
import { CreateTodoDto } from '../dtos/CreateTodoDto';
import { UpdateTodoDto } from '../dtos/UpdateTodoDto';
import { TodoService } from '../services/TodoService';

@Controller()
export class TodoController {
  constructor(private readonly todoService: TodoService) {}

  @Get('/todos')
  getTodos(@Req() req: Request) {
    return this.todoService.getAll(req['tenantId']);
  }

  @Post('/todos')
  createTodo(@Req() req: Request, @Body() data: CreateTodoDto) {
    this.todoService.create(data, req['tenantId']);
    return HttpStatus.CREATED;
  }

  @Get('/todos/:uuid')
  getTodo(@Req() req: Request, @Param('uuid') uuid: string) {
    return this.todoService.get(uuid, req['tenantId']);
  }

  @Put('/todos/:uuid')
  updateTodo(
    @Req() req: Request,
    @Param('uuid') uuid: string,
    @Body() data: UpdateTodoDto,
  ) {
    this.todoService.update(uuid, data, req['tenantId']);
    return HttpStatus.NO_CONTENT;
  }

  @Delete('/todos/:uuid')
  deleteTodo(@Req() req: Request, @Param('uuid') uuid: string) {
    this.todoService.delete(uuid, req['tenantId']);
    return HttpStatus.ACCEPTED;
  }
}

Let’s proceed by registering the todo controller and the todo service in the app.module.ts. Afterwards, we can test the todo API. Although we should be able to create our todo items, it’s important to note that we haven’t configured the tenantId yet.

Let’s configure our tenantId for our todo service.

Middlewares

Nestjs is built on top of expressjs therefore, we could use middleware. It is a function in which we can intercept of incoming HTTP requests and outgoing responses.

By using the middleware we are going to read the HTTP incoming request headers that contain tenantId for instance: x-tenant-id: f4d6f363-e4cf-4bda-af19-f0dc2feada81

We will check if the tenantId exists in our local db then add the tenantId in the request object as a property. So we can use it in the todo controller.

Otherwise, the tenantId will be null which means the todo item belongs to the host.

The final step is to implement middleware in our service, which will be our todoService. Our todo service can be accessed by invoking the /todos endpoint.

Let’s create our middleware.

Create a folder inside the src/tenant/middlewares and create a file TenantMiddleware.ts

import {
  HttpException,
  HttpStatus,
  Injectable,
  NestMiddleware,
  Logger,
} from '@nestjs/common';
import { NextFunction } from 'express';
import { TenantService } from '../services/TenantService';

@Injectable()
export class TenantMiddleware implements NestMiddleware {

  private readonly logger = new Logger(TenantMiddleware.name);
  constructor(private readonly tenantService: TenantService) {}

  async use(req: Request, res: Response, next: NextFunction) {
    const { headers } = req;

    const tenantId = headers['X-TENANT-ID'] || headers['x-tenant-id'];

    if (!tenantId) {
      this.logger.warn('`X-TENANT-ID` not provided');
      req['tenantId'] = null;
      return next();
    }
    const tenant = this.tenantService.get(tenantId);
    req['tenantId'] = tenant.id;
    next();
  }
}

Let’s register the middleware in the app.module.ts and configure it by implementing the NestModule interface.

@Module({
  imports: [],
  controllers: [AppController, TodoController, TenantController],
  providers: [TodoService, TenantService, TenantMiddleware],
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(TenantMiddleware).forRoutes('/todos');
  }
}

Let’s create 2 tenants using curl

curl -X POST --location "http://localhost:3000/api/tenants" \ -H "Content-Type: application/json" \ -d '{ "name": "Tenant 1" }'
curl -X POST --location "http://localhost:3000/api/tenants" \ -H "Content-Type: application/json" \ -d '{ "name": "Tenant 2" }'

fetch all tenants

curl -X GET --location "http://localhost:3000/api/tenants" \ -H "Accept: application/json
[
  {
    "id": "f4d6f363-e4cf-4bda-af19-f0dc2feada81",
    "name": "Tenant 1",
    "subdomain": "https://mysassapp.com"
  },
  {
    "id": "df19344b-1063-4699-809a-e596138b2194",
    "name": "Tenant 2",
    "subdomain": "https://mysassapp.com"
  },
]

Now, let's create todo items for the host, tenant 1 and tenant 2

curl -X POST --location "http://localhost:3000/api/todos" \ -H "Content-Type: application/json" \ -d '{ "title": "Belongs to host", "done": false }'
curl -X POST --location "http://localhost:3000/api/todos" \ -H "Content-Type: application/json" \ -H "X-Tenant-ID: f4d6f363-e4cf-4bda-af19-f0dc2feada81" \ -d '{ "title": "Belongs to tenant 1", "done": false }'
curl -X POST --location "http://localhost:3000/api/todos" \ -H "Content-Type: application/json" \ -H "X-Tenant-ID: df19344b-1063-4699-809a-e596138b2194" \ -d '{ "title": "Belongs to tenant 2", "done": false }'

Now fetch the todo items by passing tenantId in the request header.

curl -X GET --location "http://localhost:3000/api/todos" \ -H "Accept: application/json" \ -H "X-Tenant-ID: f4d6f363-e4cf-4bda-af19-f0dc2feada81"
[
  {
    "uuid": "0438dcc5-3061-4bf6-824d-7857885501ca",
    "title": "Belongs to tenant 1",
    "done": false,
    "tenantId": "f4d6f363-e4cf-4bda-af19-f0dc2feada81"
  }
]

If we don’t pass the tenantId in the request header we will get the todo items which belong to the host

curl -X GET --location "http://localhost:3000/api/todos" \ -H "Accept: application/json"
[
  {
    "uuid": "12922bd3-2d2b-4dff-9130-32adbaee0432",
    "title": "Belongs to host",
    "done": false
  }
]

Conclusion

We’ve successfully created a functional example of multi-tenancy with Nest.js. Nevertheless, it’s essential to consider several factors when developing multi-tenancy applications for real-world scenarios:

1. Utilize robust multi-cluster databases such as MongoDB or PostgreSQL, complemented by caching mechanisms like Redis. 2. Configure multiple database connections tailored for each tenant.

That concludes our demonstration of a basic, yet functional, implementation of multi-tenancy code with Nest.js.

That’s all! I’m certain there are numerous approaches to achieving this, and I’m eager to hear your thoughts on this post.

💡 Found this article helpful? 💡
Why not take it a step further! If you enjoyed this content and want more tips, tutorials, and hands-on insights like this, subscribe to the SajanTech Newsletter! 🚀

Join a community of developers leveling up their skills, one email at a time. Let’s build something awesome together! 🙌

Reply

or to participate.