Step4 Tenant Crud
✅ Step 4: MongoDB (Mongoose) + Tenant CRUD (UUID + Common API Response)
⚙️ 1) Update Environment Variables
Make sure .env has:
TENANT_SERVICE_HTTP_PORT=3503
TENANT_SERVICE_TCP_PORT=4503
MONGO_URI_TENANT=mongodb://localhost:27017/nestjs_tutorial_tenant_db
⚙️ 2) Install Required Dependencies
npm i @nestjs/mongoose mongoose @nestjs/config class-validator class-transformer uuid
npm i -D @types/uuid
npm install @nestjs/mapped-types
⚙️ 3) Add Common Response Utility (shared lib)
We’ll use a shared lib so all services return the same format API response.
nest g library common-lib
File: libs/common-lib/src/response.util.ts
export function apiResponse(
message: string,
data: any = null,
meta: any = null,
) {
return {
message,
data,
...(meta ? { meta } : {}),
ts: new Date().toISOString(),
};
}
File: libs/common-lib/src/index.ts
export * from './common-lib.module';
export * from './common-lib.service';
export * from './response.util';
✅ Now, every service can import:
import { apiResponse } from '@app/common-lib';
Note: After creating a new library, you will need to rerun the project. Otherwise, the lib won’t be found.
⚙️ 4) Tenant Schema (UUID + Statuses)
Create apps/tenant-service/src/schemas/tenant.schema.ts
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document } from 'mongoose';
import { v4 as uuidv4 } from 'uuid';
export type TenantDocument = Tenant & Document;
export enum TenantStatus {
PENDING = 'PENDING',
ACTIVE = 'ACTIVE',
INACTIVE = 'INACTIVE',
LOCKED = 'LOCKED',
SUSPENDED = 'SUSPENDED',
}
@Schema({ timestamps: true, versionKey: false })
export class Tenant {
@Prop({
type: String,
default: uuidv4,
})
_id: string; // UUID primary key
@Prop({ required: true, unique: true, trim: true, lowercase: true })
name: string;
@Prop({ required: true, trim: true })
displayName: string;
@Prop({ required: true, trim: true })
contactEmail: string;
@Prop({ type: String, enum: TenantStatus, default: TenantStatus.PENDING })
status: TenantStatus;
@Prop({ type: Boolean, default: false })
deleted: boolean;
}
export const TenantSchema = SchemaFactory.createForClass(Tenant);
TenantSchema.index({ name: 1 }, { unique: true });
TenantSchema.index({ status: 1, deleted: 1 });
⚙️ 5) DTOs (Validation)
Create apps/tenant-service/src/dto/
File: create-tenant.dto.ts
import { IsEmail, IsEnum, IsOptional, IsString, Matches, MinLength } from 'class-validator';
import { TenantStatus } from '../schemas/tenant.schema';
export class CreateTenantDto {
@IsString()
@MinLength(3)
@Matches(/^[a-z0-9-]+$/)
name: string;
@IsString()
@MinLength(3)
displayName: string;
@IsEmail()
contactEmail: string;
@IsOptional()
@IsEnum(TenantStatus)
status?: TenantStatus;
}
File: update-tenant.dto.ts
import { PartialType } from '@nestjs/mapped-types';
import { CreateTenantDto } from './create-tenant.dto';
export class UpdateTenantDto extends PartialType(CreateTenantDto) {}
File: change-status.dto.ts
import { IsEnum } from 'class-validator';
import { TenantStatus } from '../schemas/tenant.schema';
export class ChangeStatusDto {
@IsEnum(TenantStatus)
status: TenantStatus;
}
⚙️ 6) Update Module to Wire DB + Schema
Your file: apps/tenant-service/src/tenant-service.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { MongooseModule } from '@nestjs/mongoose';
import { TenantServiceController } from './tenant-service.controller';
import { TenantServiceService } from './tenant-service.service';
import { Tenant, TenantSchema } from './schemas/tenant.schema';
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
// DB connection
MongooseModule.forRootAsync({
useFactory: (cfg: ConfigService) => ({
uri: cfg.get<string>('MONGO_URI_TENANT'),
}),
inject: [ConfigService],
}),
// Register Tenant schema
MongooseModule.forFeature([{ name: Tenant.name, schema: TenantSchema }]),
],
controllers: [TenantServiceController],
providers: [TenantServiceService],
})
export class TenantServiceModule {}
⚙️ 7) Service Implementation
Your file: apps/tenant-service/src/tenant-service.service.ts
import { Injectable, NotFoundException, ConflictException } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model, FilterQuery } from 'mongoose';
import { Tenant, TenantDocument, TenantStatus } from './schemas/tenant.schema';
import { CreateTenantDto } from './dto/create-tenant.dto';
import { UpdateTenantDto } from './dto/update-tenant.dto';
@Injectable()
export class TenantServiceService {
constructor(
@InjectModel(Tenant.name) private readonly tenantModel: Model<TenantDocument>,
) {}
async create(dto: CreateTenantDto): Promise<Tenant> {
try {
const tenant = new this.tenantModel(dto);
return await tenant.save();
} catch (e: any) {
if (e?.code === 11000) throw new ConflictException('Tenant name already exists');
throw e;
}
}
async findAll(status?: TenantStatus): Promise<Tenant[]> {
const query: FilterQuery<Tenant> = { deleted: false };
if (status) query.status = status;
return this.tenantModel.find(query).sort({ createdAt: -1 }).lean().exec();
}
async findById(id: string): Promise<Tenant> {
const doc = await this.tenantModel.findOne({ _id: id, deleted: false }).lean().exec();
if (!doc) throw new NotFoundException('Tenant not found');
return doc;
}
async update(id: string, dto: UpdateTenantDto): Promise<Tenant> {
const updated = await this.tenantModel
.findOneAndUpdate({ _id: id, deleted: false }, { $set: dto }, { new: true, runValidators: true })
.lean().exec();
if (!updated) throw new NotFoundException('Tenant not found');
return updated;
}
async changeStatus(id: string, status: TenantStatus): Promise<Tenant> {
const updated = await this.tenantModel
.findOneAndUpdate({ _id: id, deleted: false }, { $set: { status } }, { new: true })
.lean().exec();
if (!updated) throw new NotFoundException('Tenant not found');
return updated;
}
async softDelete(id: string): Promise<{ deleted: boolean }> {
const res = await this.tenantModel
.findOneAndUpdate(
{ _id: id, deleted: false },
{ $set: { deleted: true, status: TenantStatus.INACTIVE } },
)
.lean().exec();
if (!res) throw new NotFoundException('Tenant not found');
return { deleted: true };
}
}
⚙️ 8) Controller with Response Wrapper
Your file: apps/tenant-service/src/tenant-service.controller.ts
import { Body, Controller, Delete, Get, Param, Patch, Post, Query } from '@nestjs/common';
import { TenantServiceService } from './tenant-service.service';
import { CreateTenantDto } from './dto/create-tenant.dto';
import { UpdateTenantDto } from './dto/update-tenant.dto';
import { ChangeStatusDto } from './dto/change-status.dto';
import { TenantStatus } from './schemas/tenant.schema';
import { apiResponse } from '@app/common-lib';
@Controller('tenants')
export class TenantServiceController {
constructor(private readonly service: TenantServiceService) {}
@Post()
async create(@Body() dto: CreateTenantDto) {
const data = await this.service.create(dto);
return apiResponse('Tenant created successfully', data);
}
@Get()
async findAll(@Query('status') status?: TenantStatus) {
const data = await this.service.findAll(status as TenantStatus);
return apiResponse('Tenant list fetched successfully', data);
}
@Get(':id')
async findOne(@Param('id') id: string) {
const data = await this.service.findById(id);
return apiResponse('Tenant details fetched successfully', data);
}
@Patch(':id')
async update(@Param('id') id: string, @Body() dto: UpdateTenantDto) {
const data = await this.service.update(id, dto);
return apiResponse('Tenant updated successfully', data);
}
@Patch(':id/status')
async changeStatus(@Param('id') id: string, @Body() body: ChangeStatusDto) {
const data = await this.service.changeStatus(id, body.status);
return apiResponse(`Tenant status changed to ${body.status}`, data);
}
@Delete(':id')
async softDelete(@Param('id') id: string) {
const data = await this.service.softDelete(id);
return apiResponse('Tenant deleted successfully', data);
}
}
⚙️ 9) Bootstrap (main.ts)
File: apps/tenant-service/src/main.ts
import { NestFactory } from '@nestjs/core';
import { MicroserviceOptions, Transport } from '@nestjs/microservices';
import { Logger, ValidationPipe } from '@nestjs/common';
import { TenantServiceModule } from './tenant-service.module';
async function bootstrap() {
const serviceName = 'tenant-service';
const ENV_PREFIX = serviceName.toUpperCase().replace(/-/g, '_');
const httpPort = Number(process.env[`${ENV_PREFIX}_HTTP_PORT`]) || 3503;
const tcpPort = Number(process.env[`${ENV_PREFIX}_TCP_PORT`]) || 4503;
const app = await NestFactory.create(TenantServiceModule);
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
}),
);
app.connectMicroservice<MicroserviceOptions>({
transport: Transport.TCP,
options: { host: '0.0.0.0', port: tcpPort },
});
await app.startAllMicroservices();
await app.listen(httpPort);
const logger = new Logger(serviceName);
logger.log(
`\n🚀 ${serviceName} ready!\n` +
` REST: http://localhost:${httpPort}\n` +
` TCP : tcp://localhost:${tcpPort}\n`,
);
}
bootstrap();
⚙️ 10) Example Request & Response
curl -s POST http://localhost:3503/tenants \
-H 'Content-Type: application/json' \
-d '{"name":"darmist1","displayName":"DARMIST Lab Sweden","contactEmail":"ops@darmist.com"}'
Response
{
"message": "Tenant created successfully",
"data": {
"_id": "7c3e0afc-7a8a-4e52-8c38-0d4f74f3c42b",
"name": "darmist1",
"displayName": "DARMIST Lab Sweden",
"contactEmail": "ops@darmist.com",
"status": "PENDING",
"deleted": false,
"createdAt": "2025-09-11T12:10:00.512Z",
"updatedAt": "2025-09-11T12:10:00.512Z"
},
"ts": "2025-09-11T12:10:00.512Z"
}
Open http://localhost:3503/tenants in browser to load all tenant data.
⚙️ 11) Add Tenant Microservice Endpoints (TCP handlers)
Inside apps/tenant-service/src/tenant-service.controller.ts, extend the controller with @MessagePattern handlers.
import {
Body,
Controller,
Delete,
Get,
Param,
Patch,
Post,
Query,
} from '@nestjs/common';
import { MessagePattern, Payload } from '@nestjs/microservices';
import { TenantServiceService } from './tenant-service.service';
import { CreateTenantDto } from './dto/create-tenant.dto';
import { UpdateTenantDto } from './dto/update-tenant.dto';
import { ChangeStatusDto } from './dto/change-status.dto';
import { TenantStatus } from './schemas/tenant.schema';
import { apiResponse } from '@app/common-lib';
@Controller('tenants')
export class TenantServiceController {
constructor(private readonly service: TenantServiceService) {}
// ---------- REST Endpoints ----------
@Post()
async create(@Body() dto: CreateTenantDto) {
const data = await this.service.create(dto);
return apiResponse('Tenant created successfully', data);
}
@Get()
async findAll(@Query('status') status?: TenantStatus) {
const data = await this.service.findAll(status as TenantStatus);
return apiResponse('Tenant list fetched successfully', data);
}
@Get(':id')
async findOne(@Param('id') id: string) {
const data = await this.service.findById(id);
return apiResponse('Tenant details fetched successfully', data);
}
@Patch(':id')
async update(@Param('id') id: string, @Body() dto: UpdateTenantDto) {
const data = await this.service.update(id, dto);
return apiResponse('Tenant updated successfully', data);
}
@Patch(':id/status')
async changeStatus(@Param('id') id: string, @Body() body: ChangeStatusDto) {
const data = await this.service.changeStatus(id, body.status);
return apiResponse(`Tenant status changed to ${body.status}`, data);
}
@Delete(':id')
async softDelete(@Param('id') id: string) {
const data = await this.service.softDelete(id);
return apiResponse('Tenant deleted successfully', data);
}
// ---------- Convenience REST APIs ----------
@Patch('by-name/:name/status')
async changeStatusByName(
@Param('name') name: string,
@Body() body: ChangeStatusDto,
) {
const tenant = await this.service.findByName(name); // Assuming findByName exists
const data = await this.service.changeStatus(tenant._id, body.status);
return apiResponse(`Tenant status changed to ${body.status}`, data);
}
@Get('by-name/:name/status')
async getStatusByName(@Param('name') name: string) {
const tenant = await this.service.findByName(name); // Assuming findByName exists
return apiResponse('Tenant status fetched successfully', {
name: tenant.name,
status: tenant.status,
});
}
// ---------- Microservice Endpoints (TCP) ----------
@MessagePattern({ cmd: 'tenant.create' })
async handleCreate(@Payload() dto: CreateTenantDto) {
const data = await this.service.create(dto);
return apiResponse('Tenant created successfully (TCP)', data);
}
@MessagePattern({ cmd: 'tenant.findAll' })
async handleFindAll(@Payload() status?: TenantStatus) {
const data = await this.service.findAll(status as TenantStatus);
return apiResponse('Tenant list fetched successfully (TCP)', data);
}
@MessagePattern({ cmd: 'tenant.findById' })
async handleFindById(@Payload() id: string) {
const data = await this.service.findById(id);
return apiResponse('Tenant fetched successfully (TCP)', data);
}
@MessagePattern({ cmd: 'tenant.findByName' })
async handleFindByName(@Payload() name: string) {
const data = await this.service.findByName(name);
return apiResponse('Tenant fetched successfully (TCP)', data);
}
@MessagePattern({ cmd: 'tenant.update' })
async handleUpdate(@Payload() payload: { id: string; dto: UpdateTenantDto }) {
const data = await this.service.update(payload.id, payload.dto);
return apiResponse('Tenant updated successfully (TCP)', data);
}
@MessagePattern({ cmd: 'tenant.changeStatus' })
async handleChangeStatus(
@Payload() payload: { id: string; status: TenantStatus },
) {
const data = await this.service.changeStatus(payload.id, payload.status);
return apiResponse(`Tenant status changed to ${payload.status} (TCP)`, data);
}
@MessagePattern({ cmd: 'tenant.changeStatusByName' })
async handleChangeStatusByName(
@Payload() payload: { name: string; status: TenantStatus },
) {
const tenant = await this.service.findByName(payload.name);
const data = await this.service.changeStatus(tenant._id, payload.status);
return apiResponse(
`Tenant status changed to ${payload.status} (TCP)`,
data,
);
}
@MessagePattern({ cmd: 'tenant.getStatusByName' })
async handleGetStatusByName(@Payload() name: string) {
const tenant = await this.service.findByName(name);
return apiResponse('Tenant status fetched successfully (TCP)', {
name: tenant.name,
status: tenant.status,
});
}
@MessagePattern({ cmd: 'tenant.softDelete' })
async handleSoftDelete(@Payload() id: string) {
const data = await this.service.softDelete(id);
return apiResponse('Tenant deleted successfully (TCP)', data);
}
@MessagePattern({ cmd: 'tenant.validate' })
async handleValidate(@Payload() name: string) {
try {
const tenant = await this.service.findByName(name);
return apiResponse('Tenant validated successfully (TCP)', {
valid: true,
tenant,
});
} catch {
return apiResponse('Tenant validation failed (TCP)', { valid: false });
}
}
}
⚙️ 12) Add findByName Method in Service
Extend apps/tenant-service/src/tenant-service.service.ts with a findByName method to get tenants by name:
async findByName(name: string): Promise<Tenant> {
const doc = await this.tenantModel.findOne({ name, deleted: false }).lean().exec();
if (!doc) throw new NotFoundException('Tenant not found');
return doc;
}
⚙️ 13) Add Tenant Gateway Controller
File: apps/api-gateway/src/tenant-gateway.controller.ts
import {
Body,
Controller,
Delete,
Get,
Inject,
Param,
Patch,
Post,
Query,
} from '@nestjs/common';
import { ClientProxy } from '@nestjs/microservices';
import { lastValueFrom } from 'rxjs';
import { apiResponse } from '@app/common-lib';
import { CreateTenantDto } from '../../tenant-service/src/dto/create-tenant.dto';
import { TenantStatus } from '../../tenant-service/src/schemas/tenant.schema';
import { UpdateTenantDto } from '../../tenant-service/src/dto/update-tenant.dto';
import { ChangeStatusDto } from '../../tenant-service/src/dto/change-status.dto';
@Controller('gateway/tenants')
export class TenantGatewayController {
constructor(
@Inject('TENANT_SERVICE') private readonly tenantClient: ClientProxy,
) {}
@Post()
async create(@Body() dto: CreateTenantDto) {
const result = await lastValueFrom(
this.tenantClient.send({ cmd: 'tenant.create' }, dto),
);
return apiResponse('Tenant created via Gateway', result.data);
}
@Get()
async findAll(@Query('status') status?: TenantStatus) {
const result = await lastValueFrom(
this.tenantClient.send({ cmd: 'tenant.findAll' }, status ?? ''),
);
return apiResponse('Tenant list via Gateway', result.data);
}
@Get(':id')
async findById(@Param('id') id: string) {
const result = await lastValueFrom(
this.tenantClient.send({ cmd: 'tenant.findById' }, id),
);
return apiResponse('Tenant fetched via Gateway', result.data);
}
@Get('by-name/:name')
async findByName(@Param('name') name: string) {
const result = await lastValueFrom(
this.tenantClient.send({ cmd: 'tenant.findByName' }, name),
);
return apiResponse('Tenant fetched via Gateway', result.data);
}
@Patch(':id')
async update(@Param('id') id: string, @Body() dto: UpdateTenantDto) {
const result = await lastValueFrom(
this.tenantClient.send({ cmd: 'tenant.update' }, { id, dto }),
);
return apiResponse('Tenant updated via Gateway', result.data);
}
@Patch(':id/status')
async changeStatus(@Param('id') id: string, @Body() body: ChangeStatusDto) {
const result = await lastValueFrom(
this.tenantClient.send(
{ cmd: 'tenant.changeStatus' },
{ id, status: body.status },
),
);
return apiResponse(
`Tenant status changed to ${body.status} via Gateway`,
result.data,
);
}
@Patch('by-name/:name/status')
async changeStatusByName(
@Param('name') name: string,
@Body() body: ChangeStatusDto,
) {
const result = await lastValueFrom(
this.tenantClient.send(
{ cmd: 'tenant.changeStatusByName' },
{ name, status: body.status },
),
);
return apiResponse(
`Tenant status changed to ${body.status} via Gateway`,
result.data,
);
}
@Get('by-name/:name/status')
async getStatusByName(@Param('name') name: string) {
const result = await lastValueFrom(
this.tenantClient.send({ cmd: 'tenant.getStatusByName' }, name),
);
return apiResponse('Tenant status via Gateway', result.data);
}
@Delete(':id')
async softDelete(@Param('id') id: string) {
const result = await lastValueFrom(
this.tenantClient.send({ cmd: 'tenant.softDelete' }, id),
);
return apiResponse('Tenant deleted via Gateway', result.data);
}
@Get('validate/:name')
async validateTenant(@Param('name') name: string) {
const result = await lastValueFrom(
this.tenantClient.send({ cmd: 'tenant.validate' }, name),
);
return apiResponse('Tenant validation via Gateway', result.data);
}
}
⚙️ 14) Configure API Gateway to Call Tenant Service
We connect API Gateway to Tenant Service over TCP.
Remember to add TenantGatewayController in the controllers list. Otherwise, api gateway endpoints won’t be available.
File: apps/api-gateway/src/api-gateway.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { ClientsModule, Transport } from '@nestjs/microservices';
import { ApiGatewayController } from './api-gateway.controller';
import { ApiGatewayService } from './api-gateway.service';
import { TenantGatewayController } from './tenant-gateway.controller';
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
ClientsModule.registerAsync([
{
name: 'AUTH_SERVICE',
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (cfg: ConfigService) => ({
transport: Transport.TCP,
options: {
host: '0.0.0.0',
port: Number(cfg.get('AUTH_SERVICE_TCP_PORT') || 4502),
},
}),
},
{
name: 'TENANT_SERVICE',
inject: [ConfigService],
useFactory: (cfg: ConfigService) => ({
transport: Transport.TCP,
options: {
host: '0.0.0.0',
port: cfg.get<number>('TENANT_SERVICE_TCP_PORT', 4503),
},
}),
},
{
name: 'USER_SERVICE',
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (cfg: ConfigService) => ({
transport: Transport.TCP,
options: {
host: '0.0.0.0',
port: Number(cfg.get('USER_SERVICE_TCP_PORT') || 4504),
},
}),
},
// (You’ll add PRODUCT_SERVICE similarly later)
]),
],
controllers: [ApiGatewayController, TenantGatewayController],
providers: [ApiGatewayService],
})
export class ApiGatewayModule {}
⚙️ 15) Verify the Flow
Run both services:
npx nest start tenant-service --watch
npx nest start api-gateway --watch
Test REST directly on Tenant Service
curl -s http://localhost:3503/tenants/by-name/darmist1/status | jq
Test through API Gateway
curl -s http://localhost:3501/gateway/tenants/by-name/darmist1/status | jq
Expected Response
{
"message": "Tenant status fetched via Gateway",
"data": {
"name": "darmist1",
"status": "ACTIVE"
},
"ts": "2025-09-11T14:20:15.512Z"
}
⚙️ 16) Test with cURL Commands
Create a tenant
curl -X POST http://localhost:3501/gateway/tenants \
-H "Content-Type: application/json" \
-d '{
"name": "skyflow1",
"displayName": "SkyFlow Technologies",
"contactEmail": "contact@skyflow.com"
}'
Get all tenants
curl -X GET "http://localhost:3501/gateway/tenants"
With status filter:
curl -X GET "http://localhost:3501/gateway/tenants?status=ACTIVE"
Get tenant by ID
curl -X GET http://localhost:3501/gateway/tenants/2f4a1c8d-9b67-4db0-92a7-1a9c2e8d45ef
Get tenant by name
curl -X GET http://localhost:3501/gateway/tenants/by-name/skyflow1
Update a tenant
curl -X PATCH http://localhost:3501/gateway/tenants/2f4a1c8d-9b67-4db0-92a7-1a9c2e8d45ef \
-H "Content-Type: application/json" \
-d '{
"displayName": "SkyFlow Global",
"contactEmail": "support@skyflow.com"
}'
Change tenant status by ID
curl -X PATCH http://localhost:3501/gateway/tenants/2f4a1c8d-9b67-4db0-92a7-1a9c2e8d45ef/status \
-H "Content-Type: application/json" \
-d '{
"status": "SUSPENDED"
}'
Change tenant status by name
curl -X PATCH http://localhost:3501/gateway/tenants/by-name/skyflow1/status \
-H "Content-Type: application/json" \
-d '{
"status": "ACTIVE"
}'
Get tenant status by name
curl -X GET http://localhost:3501/gateway/tenants/by-name/skyflow1/status
Soft delete tenant
curl -X DELETE http://localhost:3501/gateway/tenants/2f4a1c8d-9b67-4db0-92a7-1a9c2e8d45ef
Validate tenant by name
curl -X GET http://localhost:3501/gateway/tenants/validate/skyflow1
⚙️ 17) Pagination Support
Now, we will implement paginated data loading for tenant records.
✅ tenant-service.controller.ts
@Get()
async findAll(
@Query('status') status?: TenantStatus,
@Query('page') page?: number,
@Query('pageSize') pageSize?: number,
) {
const { data, total, meta } = await this.service.findAll(
status as TenantStatus,
page,
pageSize,
);
return apiResponse('Tenant list fetched successfully', data, meta);
}
// ---------- TCP ----------
@MessagePattern({ cmd: 'tenant.findAll' })
async handleFindAll(
@Payload() payload: { status?: TenantStatus; page?: number; pageSize?: number },
) {
const { data, total, meta } = await this.service.findAll(
payload.status as TenantStatus,
payload.page,
payload.pageSize,
);
return apiResponse('Tenant list fetched successfully (TCP)', data, meta);
}
✅ tenant-service.service.ts
async findAll(
status?: TenantStatus,
page = 1,
pageSize = 10,
): Promise<{ data: Tenant[]; total: number; meta: any }> {
const query: FilterQuery<Tenant> = { deleted: false };
if (status) query.status = status;
const total = await this.tenantModel.countDocuments(query);
// Negative page means "all data"
if (page < 0) {
const data = await this.tenantModel.find(query).sort({ createdAt: -1 }).lean().exec();
return {
data,
total,
meta: {
total,
page: -1,
pageSize: total,
totalPages: 1,
},
};
}
const skip = (page - 1) * pageSize;
const data = await this.tenantModel
.find(query)
.sort({ createdAt: -1 })
.skip(skip)
.limit(pageSize)
.lean()
.exec();
return {
data,
total,
meta: {
total,
page,
pageSize,
totalPages: Math.ceil(total / pageSize) || 1,
},
};
}
✅ tenant-gateway.controller.ts
@Get()
async findAll(
@Query('status') status?: TenantStatus,
@Query('page') page?: number,
@Query('pageSize') pageSize?: number,
) {
const result = await lastValueFrom(
this.tenantClient.send(
{ cmd: 'tenant.findAll' },
{
status: status ?? null,
page: page !== undefined ? Number(page) : 1,
pageSize: pageSize !== undefined ? Number(pageSize) : 10,
},
),
);
return apiResponse('Tenant list via Gateway', result.data, result.meta);
}
✅ Example Calls Default (page=1, pageSize=10):
curl -s "http://localhost:3501/gateway/tenants" |jq
Specific page:
curl -s "http://localhost:3501/gateway/tenants?page=2&pageSize=5"
All data (page < 0):
curl -s "http://localhost:3501/gateway/tenants?page=-1"