nestjs-multitenant
v2.1.1
Published
A comprehensive multi-tenant solution for NestJS applications with PostgreSQL schema-per-tenant architecture
Maintainers
Readme
nestjs-multitenant
Una solución completa de multi-tenancy para aplicaciones NestJS con arquitectura de esquema por tenant (PostgreSQL) y utilidades para inyección de repositorios, middlewares de resolución de tenant y configuración tipada.
📖 Read the Full Documentation - Comprehensive guides, API reference, and examples
🚀 Características
- Arquitectura Schema-per-Tenant: Cada tenant tiene su propio esquema de base de datos
- Soporte Multi-ORM: Compatible con TypeORM y Drizzle ORM
- Migraciones Automáticas: Migraciones internas para módulo admin (solo Drizzle)
- Resolución Automática de Tenants: Soporte para múltiples estrategias (header, subdomain, JWT, custom)
- Pool de Conexiones Dinámico: Gestión eficiente de conexiones por tenant
- Inyección de Repositorios: Decoradores para inyectar repositorios específicos del tenant
- Administración de Tenants: Módulo completo para CRUD de tenants
- Registro de Entidades: Sistema flexible para configurar entidades por tenant
- TypeScript: Completamente tipado con soporte completo de TypeScript
- Escalable: Diseñado para aplicaciones de gran escala
Instalación
Instala el paquete y sus peer dependencies requeridas:
pnpm add nestjs-multitenantDependencias Peer
Para TypeORM (opción tradicional):
npm install @nestjs/common @nestjs/core @nestjs/typeorm @nestjs/config typeorm pgPara Drizzle ORM (recomendado para mejor control de migraciones):
npm install @nestjs/common @nestjs/core @nestjs/config drizzle-orm pg
npm install -D drizzle-kitRequisitos: Node.js >= 22, TypeScript >= 5.9, NestJS 11
📌 Importante: Drizzle ORM es recomendado para multi-tenancy avanzado. Ver Migraciones con Drizzle para más detalles.
🛠️ Configuración Básica
1. Configurar el Módulo Principal (forRoot)
Importa el módulo en tu aplicación e inicializa la configuración:
// app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { MultiTenantModule } from 'nestjs-multitenant';
@Module({
imports: [
// ⚠️ IMPORTANTE: ConfigModule debe importarse ANTES que MultiTenantModule
ConfigModule.forRoot({
isGlobal: true, // Hace que ConfigService esté disponible globalmente
}),
// Configuración de la base de datos principal
TypeOrmModule.forRoot({
type: 'postgres',
host: 'localhost',
port: 5432,
username: 'postgres',
password: 'password',
database: 'multitenant_db',
schema: 'public',
synchronize: true,
}),
// Configuración del módulo multi-tenant
MultiTenantModule.forRoot({
database: {
host: 'localhost',
port: 5432,
username: 'postgres',
password: 'password',
database: 'multitenant_db',
},
autoCreateSchemas: true,
enableAdminModule: true, // Habilita el módulo de administración
platform: 'express', // express o fastify
customControllers: [CustomTenantAdminController], // Permite cargar tu propia implementacion del controller, omitiendo el controller administrativo, si no se especifica tomara la implementacion propia del modulo interno
customProviders: [
createTenantStrategyProvider(TenantAdminService), // Type-safe
],
}),
],
})
export class AppModule {}Nota importante sobre forRoot:
- La propiedad
enableAdminModule, precargara la claseTenantAdminServicey solo si la propiedadcustomControllersse encuentra vacia cargaraTenantAdminController; esto brinda la ventaja de especificar tu propio controller. - Puedes omitir la clase precargada
TenantAdminService, haciendo uso de la propiedadcustomProvidersy especificar tu propia implementación.
2. Configuración Asíncrona (forRootAsync) - v2.1.0+
Para configuraciones dinámicas que requieren inyección de dependencias:
:::note Simplified Configuration (v2.1.0+)
El método buildAsyncConfig() simplifica drásticamente la configuración asíncrona. Ver ejemplos completos en Configuración Avanzada.
:::
Configuración recomendada con controller admin por defecto:
// app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import {
MultiTenantModule,
createDatabaseConfigFromEnv,
} from 'nestjs-multitenant';
@Module({
imports: [
// ⚠️ IMPORTANTE: ConfigModule debe importarse ANTES que MultiTenantModule
ConfigModule.forRoot({ isGlobal: true }),
// Conexión principal de la aplicación
TypeOrmModule.forRoot({
type: 'postgres',
host: 'localhost',
port: 5432,
username: 'postgres',
password: 'postgres',
database: 'multitenant_db',
schema: 'public',
synchronize: true,
}),
// 🆕 Configuración simplificada con buildAsyncConfig (v2.1.0+)
MultiTenantModule.forRootAsync(
MultiTenantModule.buildAsyncConfig({
ormType: 'typeorm',
enableAdminController: true, // Incluye controller admin por defecto
useFactory: (config: ConfigService) => ({
database: createDatabaseConfigFromEnv(config),
validationStrategy: 'local',
autoCreateSchemas: true,
platform: 'fastify',
}),
inject: [ConfigService],
}),
),
],
})
export class AppModule {}Configuración con controller personalizado:
// app.module.ts
MultiTenantModule.forRootAsync(
MultiTenantModule.buildAsyncConfig({
ormType: 'typeorm',
enableAdminController: false, // No incluye controller por defecto
additionalImports: [CustomTenantAdminModule], // Importa el módulo personalizado con el controller
managementStrategyProvider: createTenantStrategyProvider(
CustomTenantAdminService,
),
useFactory: (config: ConfigService) => ({
database: createDatabaseConfigFromEnv(config),
validationStrategy: 'local',
autoCreateSchemas: true,
platform: 'fastify',
}),
inject: [ConfigService],
}),
);Nota importante sobre buildAsyncConfig (v2.1.0+):
- 🆕 Simplificado: Maneja automáticamente la configuración específica del ORM
- 🆕 Type-Safe:
createTenantControllerFactory()para validación en tiempo de compilación - 🆕 Flexible: Soporta TypeORM y Drizzle con la misma API
- ✅ Legacy forRootAsync: Aún disponible pero marcado como deprecated
- 📖 Documentación completa: Ver Configuración Avanzada para todos los escenarios
3. Registro de Entidades para forRootAsync
Cuando uses forRootAsync, es CRÍTICO registrar las entidades ANTES de que se inicialice el módulo:
// entities/index.ts - Crear este archivo PRIMERO
import { EntityRegistry } from 'nestjs-multitenant';
import { User } from './user.entity';
import { Product } from './product.entity';
// IMPORTANTE: Registrar INMEDIATAMENTE al importar
// Opción 1: API fluida (recomendada)
EntityRegistry.getInstance()
.registerEntity('User', User)
.registerEntity('Product', Product);
// Opción 2: Registrar múltiples entidades
export const entities = {
User: User,
Product: Product,
};
EntityRegistry.getInstance().registerEntities(entities);// app.module.ts
import { Module } from '@nestjs/common';
import './entities'; // IMPORTAR PRIMERO para registrar entidades
import { MultiTenantModule } from 'nestjs-multitenant';
@Module({
imports: [
// ... otros módulos
MultiTenantModule.forRootAsync({
// ... configuración
}),
],
})
export class AppModule {}⚠️ Problema Común: Si registras las entidades después de la inicialización del módulo, getEntityRegistryConfig() retornará un objeto entities vacío.
✅ Solución: Siempre importa el archivo de registro de entidades ANTES que MultiTenantModule.forRootAsync.
🔍 Debug: Usa getEntityRegistryDebugInfo() para verificar el estado del registro:
import { getEntityRegistryDebugInfo } from 'nestjs-multitenant';
// En un endpoint o servicio
const debugInfo = getEntityRegistryDebugInfo();
console.log('Registry state:', debugInfo);
// Output: { entityCount: 2, entities: ['User', 'Product'], presets: ['basic', 'full'] }4. Variables de Entorno
# .env
DB_TYPE=postgres
DB_HOST=localhost
DB_PORT=5432
DB_USERNAME=postgres
DB_PASSWORD=password
DB_DATABASE=multitenant_db
TENANT_HEADER=x-tenant-id
AUTO_CREATE_SCHEMAS=true
ENABLE_ADMIN_MODULE=true
MULTITENANT_RUN_ADMIN_MIGRATIONS=true # Para Drizzle (default: true)📌 Nota:
MULTITENANT_RUN_ADMIN_MIGRATIONScontrola las migraciones automáticas del módulo admin (solo para Drizzle ORM).
🏗️ Uso del Sistema
Definir Entidades
Para TypeORM:
// entities/user.entity.ts
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
@Entity('users')
export class User {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
name: string;
@Column({ unique: true })
email: string;
@Column()
createdAt: Date;
}Para Drizzle ORM:
// entities/user.entity.ts
import { pgTable, uuid, varchar, timestamp } from 'drizzle-orm/pg-core';
import { prefixSchema } from 'nestjs-multitenant';
// Usar el patrón de schemas recomendado
export const userSchema = prefixSchema('tenant_example');
export const users = userSchema.table('users', {
id: uuid('id').defaultRandom().primaryKey(),
name: varchar('name', { length: 255 }).notNull(),
email: varchar('email', { length: 255 }).notNull().unique(),
createdAt: timestamp('created_at').defaultNow(),
});📖 Drizzle Setup: Para guía completa de configuración de Drizzle, ver Setup PostgreSQL
Registrar Entidades
// entities/index.ts
import { EntityRegistry } from 'nestjs-multitenant';
import { User } from './user.entity';
import { Product } from './product.entity';
// Opción 1: Registrar entidades individualmente (API fluida)
EntityRegistry.getInstance()
.registerEntity('User', User)
.registerEntity('Product', Product)
.registerPreset('basic', ['User'])
.registerPreset('ecommerce', ['User', 'Product']);
// Opción 2: Registrar múltiples entidades a la vez
const entities = {
User: User,
Product: Product,
};
EntityRegistry.getInstance()
.registerEntities(entities)
.registerPresets({
basic: ['User'],
ecommerce: ['User', 'Product'],
});Crear un Servicio con Repositorios de Tenant
// services/user.service.ts
import { Injectable } from '@nestjs/common';
import { Repository } from 'typeorm';
import { InjectTenantRepository } from 'nestjs-multitenant';
import { User } from '../entities/user.entity';
@Injectable()
export class UserService {
constructor(
@InjectTenantRepository(User)
private readonly userRepository: Repository<User>,
) {}
async findAll(): Promise<User[]> {
return this.userRepository.find();
}
async create(userData: Partial<User>): Promise<User> {
const user = this.userRepository.create(userData);
return this.userRepository.save(user);
}
async findById(id: string): Promise<User> {
return this.userRepository.findOne({ where: { id } });
}
}Configurar un Módulo de Funcionalidad
// modules/user.module.ts
import { Module } from '@nestjs/common';
import { createTenantRepositoryProviders } from 'nestjs-multitenant';
import { User } from '../entities/user.entity';
import { UserService } from '../services/user.service';
import { UserController } from '../controllers/user.controller';
@Module({
providers: [...createTenantRepositoryProviders([User]), UserService],
controllers: [UserController],
exports: [UserService],
})
export class UserModule {}🔧 Configuración Avanzada
Estrategias de Resolución de Tenants
- Header: Extrae el tenant ID de un encabezado HTTP.
- Subdominio: Utiliza el subdominio como tenant ID.
- Dominio: Extrae el tenant ID del dominio.
- Query Parameter: Obtiene el tenant ID de un parámetro de consulta.
MultiTenantModule.forRootAsync({
tenantResolution: {
strategy: 'header' | 'subdomain' | 'jwt' | 'custom',
headerName: 'x-tenant-id', // Encabezado por defecto
jwtClaimName: 'tenantId', // Claim por defecto en JWT
customResolver: (request: unknown) => {
// Lógica personalizada para resolver tenant
return 'default-tenant';
},
},
});Nota Importante:
- Si la estrategia de resolución es
header, asegúrate de que el encabezado exista en la solicitud y definirheaderNamesi es diferente dex-tenant-id. - Si la estrategia de resolución es
subdomain, asegúrate de que el subdominio esté configurado correctamente. - Si la estrategia de resolución es
jwt, asegúrate de que el token JWT esté presente y válido; ademas, definirjwtClaimNamesi es diferente detenantId. - Si la estrategia de resolución es
custom, asegúrate de proporcionar una función personalizada que devuelva el tenant ID basado en la solicitud.
Pool de Conexiones
MultiTenantModule.forRoot({
// ... otras configuraciones
connectionPool: {
maxConnections: 50,
idleTimeout: 30000,
enableCleanup: 30000,
cleanupInterval: 5000,
},
});Configuración de Entidades por Tenant
// Configurar entidades disponibles por tenant
EntityRegistry.getInstance()
.registerPreset('startup', ['User', 'Project'])
.registerPreset('enterprise', ['User', 'Project', 'Analytics', 'Billing']);🔍 Uso Avanzado
Acceso Directo al DataSource del Tenant
import { Injectable } from '@nestjs/common';
import { DataSource } from 'typeorm';
import { InjectTenantDataSource } from 'nestjs-multitenant';
@Injectable()
export class AdvancedService {
constructor(
@InjectTenantDataSource()
private readonly dataSource: DataSource,
) {}
async executeRawQuery(query: string) {
return this.dataSource.query(query);
}
async runTransaction(callback: (manager: EntityManager) => Promise<any>) {
return this.dataSource.transaction(callback);
}
}Factory de Repositorios
import { Injectable } from '@nestjs/common';
import { Repository } from 'typeorm';
import { InjectTenantRepositoryFactory } from 'nestjs-multitenant';
import { User } from '../entities/user.entity';
@Injectable()
export class MultiTenantService {
constructor(
@InjectTenantRepositoryFactory(User)
private readonly userRepositoryFactory: (
tenantId: string,
) => Promise<Repository<User>>,
) {}
async getUsersFromSpecificTenant(tenantId: string) {
const userRepository = await this.userRepositoryFactory(tenantId);
return userRepository.find();
}
}🔄 Migraciones con Drizzle ORM
Para proyectos con Drizzle ORM, el módulo incluye capacidades avanzadas de migración:
Configuración Automática
El módulo gestiona automáticamente las migraciones del schema de administración:
// drizzle.config.ts
import type { Config } from 'drizzle-kit';
export default {
schema: './src/entities/**/*.ts',
out: './drizzle',
driver: 'pg',
dbCredentials: {
url: process.env.DATABASE_URL,
},
} satisfies Config;Flujo de Trabajo
- Generar migración:
drizzle-kit generate - Aplicar migración:
drizzle-kit migrate - Migraciones admin: Automáticas controladas por
MULTITENANT_RUN_ADMIN_MIGRATIONS
📖 Guía Completa: Ver Migraciones con Drizzle para detalles completos.
Problemas Comunes
Errores de Migración con Drizzle
Problema: Las migraciones de tenant no se aplican correctamente.
Solución: Asegúrate de seguir el patrón prefixSchema:
const prefixSchema = (schema: string) => pgSchema(`tenant_${schema}`);
export const tenantSchema = prefixSchema('your-tenant-id');Problema: Migraciones del módulo admin no se ejecutan.
Solución: Verifica la variable de entorno:
MULTITENANT_RUN_ADMIN_MIGRATIONS=trueConexión a Base de Datos
Problema: Errores de conexión o timeout.
Solución: Verifica la configuración de la base de datos y asegúrate de que PostgreSQL esté ejecutándose:
MultiTenantModule.forRoot({
database: {
host: 'localhost',
port: 5432,
username: 'postgres',
password: 'password',
database: 'multitenant_db',
// Opcional: configuración adicional
ssl: false,
synchronize: true, // Solo en desarrollo
logging: true, // Para debug
},
});🔧 Resolución de Problemas
Error: "Nest can't resolve dependencies of the TypeOrmModuleOptions"
Problema: Error al inicializar el módulo con el mensaje:
UnknownDependenciesException [Error]: Nest can't resolve dependencies of the TypeOrmModuleOptions (?).
Please make sure that the argument ConfigService at index [0] is available in the TypeOrmCoreModule context.Solución: Este error ocurre cuando ConfigModule no está disponible en el contexto del módulo. Asegúrate de:
- Importar ConfigModule ANTES que MultiTenantModule:
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }), // ⚠️ DEBE ir ANTES
MultiTenantModule.forRoot({ /* config */ }),
],
})- Usar configuración global:
ConfigModule.forRoot({
isGlobal: true, // Hace ConfigService disponible globalmente
});- Para configuración asíncrona, inyectar ConfigService correctamente:
MultiTenantModule.forRootAsync({
inject: [ConfigService],
useFactory: (configService: ConfigService) => ({
// configuración
}),
});Error: "Cannot find module 'nestjs-multitenant'"
Solución: Instala las dependencias requeridas:
Para TypeORM:
npm install @nestjs/common @nestjs/core @nestjs/typeorm @nestjs/config typeorm pgPara Drizzle ORM:
npm install @nestjs/common @nestjs/core @nestjs/config drizzle-orm pg
npm install -D drizzle-kitProblemas de Conexión a Base de Datos
Problema: Errores de conexión o timeout.
Solución: Verifica la configuración de la base de datos y asegúrate de que PostgreSQL esté ejecutándose:
MultiTenantModule.forRoot({
database: {
host: 'localhost',
port: 5432,
username: 'postgres',
password: 'password',
database: 'multitenant_db',
// Opcional: configuración adicional
ssl: false,
synchronize: true, // Solo en desarrollo
logging: true, // Para debug
},
});🤝 Contribución
- Fork el proyecto
- Crea una rama para tu feature (
git checkout -b feature/amazing-feature) - Commit tus cambios (
git commit -m 'Add amazing feature') - Push a la rama (
git push origin feature/amazing-feature) - Abre un Pull Request
Versionado y CHANGELOG
Este proyecto sigue SemVer. Las releases se realizan con mensajes de
commit semánticos y se documentan en CHANGELOG.md.
🆘 Soporte
- 📧 Email: [email protected]
- 🐛 Issues: GitHub Issues
- 📖 Documentación: Docs
Licencia
MIT © Reymi Tech
