Package Exports
- nestjs-multitenant
- nestjs-multitenant/package.json
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)
Para configuraciones más complejas que requieren inyección de dependencias:
// 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: [
// ConfigModule DEBE ser importado 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,
}),
// Conexión de la base de datos admin
TypeOrmModule.forRootAsync({
name: 'admin',
inject: [ConfigService],
useFactory: (configService: ConfigService) =>
getAdminDatabaseConfig(configService),
}),
// Configuración asíncrona del módulo multi-tenant
MultiTenantModule.forRootAsync({
inject: [ConfigService],
imports: [
TypeOrmModule.forFeature([Tenant], 'admin'),
CustomTenantAdminModule, // Importa tu módulo con controllers, mediante wrapper. Opcion 1
],
useFactory: (configService: ConfigService) => ({
database: createDatabaseConfigFromEnv(configService),
autoCreateSchemas: configService.get<boolean>(
'AUTO_CREATE_SCHEMAS',
true,
),
enableAdminModule: configService.get<boolean>(
'ENABLE_ADMIN_MODULE',
true,
),
validationStrategy: 'local',
platform: configService.get<string>(
'PLATFORM',
'express',
) as PlatformType,
// customControllers: [CustomTenantAdminController] // Opcion 2
}),
// controllers: [CustomTenantAdminController], // Opcion 3
// OPCIÓN: Usa tu propia implementación de management
managementStrategyProvider:
createTenantStrategyProvider(TenantAdminService), // Garantiza un type-safe
}),
],
})
export class AppModule {}Nota importante sobre forRootAsync:
- Ahora solo incluye las funcionalidades de administración (
TenantAdminService), se debera indicar el controlador a usar mediante wrapper, customControllers o controllers. - La opción
enableAdminModulecontrola solo siTenantAdminServicefuncionalidades están activas en tiempo de ejecución - La configuración de base de datos admin se toma directamente de las variables de entorno (no del parámetro
database) - Es ideal para configuraciones que dependen de variables de entorno o servicios externos
- Puedes omitir la clase Precargada
TenantAdminServicey compartir tu propia implementación mediante la propieadadmanagementStrategyProvider
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: lmorochofebres@gmail.com
- 🐛 Issues: GitHub Issues
- 📖 Documentación: Docs
Licencia
MIT © Reymi Tech