Imported from afperdomo2/products-launcher (
AGENTS.md). Install upstream withnpx skills add afperdomo2/products-launcher. Copyright stays with the author.
AGENTS.md — products-launcher
Repository Overview
This is a Git submodule monorepo that orchestrates 5 independent NestJS microservices:
| Submodule | Role | Database |
|---|---|---|
client-gateway/ |
HTTP API Gateway (Express + REST) | None |
auth-ms/ |
Authentication & JWT | MongoDB (Prisma) |
products-ms/ |
Products CRUD | SQLite (Prisma) |
orders-ms/ |
Orders management | PostgreSQL (Prisma) |
payments-ms/ |
PayPal payment processing | None |
All services communicate via NATS messaging. There is no root-level package.json; all commands must be run from within a specific service directory.
Build / Run Commands
All commands are run from inside a service directory (e.g., cd products-ms).
npm install # Install dependencies
npm run build # Compile TypeScript → dist/ via NestJS CLI
npm run start:dev # Run with hot-reload (also runs Prisma migrations if applicable)
npm run start:prod # Run compiled output: node dist/main
npm run start:debug # Debug mode with hot-reload
Full Stack (Docker — preferred for dev)
# From repo root
docker compose -f docker-compose.dev.yaml up --build # Start all services + NATS + databases
docker compose -f docker-compose.prod.yaml up --build # Production build
Copy .env.example to .env and fill in values before running Docker Compose.
Lint / Format Commands
Run from within a service directory:
npm run lint # ESLint with --fix (TypeScript, src/ and test/)
npm run format # Prettier --write on src/**/*.ts and test/**/*.ts
Test Commands
Run from within a service directory:
npm test # Run all unit tests once
npm run test:watch # Watch mode
npm run test:cov # With coverage report
npm run test:e2e # E2E tests (jest-e2e.json config)
npm run test:debug # Debug with --inspect-brk
# Run a single test file
npx jest src/products/products.service.spec.ts
# Run tests matching a file name pattern
npm test -- --testPathPattern="products.service"
# Run tests matching a test name
npm test -- --testNamePattern="should create a product"
# Run a single file in watch mode
npm run test:watch -- --testPathPattern="products.service"
Unit test files are *.spec.ts co-located in src/. E2E tests are *.e2e-spec.ts in test/.
Code Style Guidelines
Formatting (Prettier)
- Single quotes for strings
- Trailing commas everywhere (
"all") - LF line endings
- Prettier is enforced via ESLint (
plugin:prettier/recommended), sonpm run lintalso catches format issues.
TypeScript
- Target:
ES2021, module system:CommonJS strictmode is OFF —strictNullChecks,noImplicitAny, andstrictBindCallApplyare all disabledanyis explicitly allowed (ESLint rule@typescript-eslint/no-explicit-anyis off)- Decorators are required:
emitDecoratorMetadata: true,experimentalDecorators: true skipLibCheck: true
Imports
Group and order imports as follows (blank line between groups):
// 1. NestJS framework
import { Body, Controller, Get, Param, ParseIntPipe } from '@nestjs/common';
import { MessagePattern, Payload } from '@nestjs/microservices';
// 2. Third-party libraries
import { firstValueFrom } from 'rxjs';
import { IsOptional, IsPositive } from 'class-validator';
// 3. Internal absolute paths (from src/)
import { PaginationDto } from 'src/common/dto/pagination.dto';
import { envs } from 'src/config';
// 4. Relative local imports
import { CreateProductDto } from './dto/create-product.dto';
import { ProductsService } from './products.service';
No import sorting tool is configured; follow this convention manually.
Naming Conventions
| Concept | Convention | Example |
|---|---|---|
| Files | kebab-case + NestJS suffix |
products.controller.ts, create-product.dto.ts |
| Classes | PascalCase + NestJS suffix |
ProductsController, CreateProductDto |
| Interfaces | PascalCase, no I prefix |
CurrentUser, JwtPayload |
| Enums (type) | PascalCase |
OrderStatus, Services |
| Enum values | SCREAMING_SNAKE_CASE |
OrderStatus.PENDING, Services.NATS_SERVICE |
| Variables / methods | camelCase |
createOrder, findAll, jwtSecret |
Env vars (.env) |
SCREAMING_SNAKE_CASE |
JWT_SECRET, NATS_SERVERS |
| Barrel files | index.ts re-exporting with export * from |
config/index.ts, enums/index.ts |
NestJS Patterns
Services with Prisma extend PrismaClient directly and implement OnModuleInit:
@Injectable()
export class ProductsService extends PrismaClient implements OnModuleInit {
private readonly logger = new Logger('ProductsService');
async onModuleInit() {
await this.$connect();
this.logger.log('Database connected');
}
}
Microservice controllers use @MessagePattern (request/response) or @EventPattern (fire-and-forget) with @Payload():
@Controller()
export class ProductsController {
@MessagePattern({ cmd: 'findOne' })
findOne(@Payload('id', ParseIntPipe) id: number) {
return this.productsService.findOne(id);
}
}
Logger pattern — always use NestJS Logger with the class name as context:
private readonly logger = new Logger('ClassName');
// Usage:
this.logger.log('Message');
this.logger.error('Error message');
Error Handling
In microservices — throw RpcException with a structured object:
import { RpcException } from '@nestjs/microservices';
import { HttpStatus } from '@nestjs/common';
throw new RpcException({
statusCode: HttpStatus.NOT_FOUND,
message: `Product #${id} not found`,
});
In the client-gateway — two accepted patterns for consuming microservice responses:
// Pattern 1: async/await with firstValueFrom
try {
return await firstValueFrom(this.client.send({ cmd: 'findOne' }, { id }));
} catch (error) {
throw new RpcException(error);
}
// Pattern 2: Observable pipe with catchError
return this.client.send({ cmd: 'remove' }, { id }).pipe(
catchError((err) => { throw new RpcException(err); }),
);
A global RpcCustomExceptionFilter (in client-gateway/src/common/exceptions/) maps RpcException payloads back to proper HTTP responses. Simple read endpoints may return the Observable directly.
Environment Configuration
Each service validates its environment variables at startup with joi. The canonical pattern:
// src/config/envs.config.ts
import 'dotenv/config';
import * as joi from 'joi';
const envsSchema = joi.object({
PORT: joi.number().required(),
JWT_SECRET: joi.string().required(),
}).unknown(true);
const { error, value: envVars } = envsSchema.validate(process.env);
if (error) throw new Error(`Config validation error: ${error.message}`);
export const envs = {
port: envVars.PORT as number,
jwtSecret: envVars.JWT_SECRET as string,
};
Export from src/config/index.ts as a barrel: export * from './envs.config';
DTOs
Use class-validator decorators for validation and class-transformer decorators for transformation. Always use @Type(() => ...) when transforming nested types or numbers from query strings:
import { IsOptional, IsPositive, Min } from 'class-validator';
import { Type } from 'class-transformer';
export class PaginationDto {
@IsOptional()
@IsPositive()
@Type(() => Number)
page?: number = 1;
@IsOptional()
@Min(1)
@Type(() => Number)
limit?: number = 10;
}
Project-Specific Notes
- No CI/CD pipelines exist — deployment is manual via Docker Hub / Azure Container Registry.
- Submodule changes must be committed and pushed in the individual submodule repo, then the parent repo's submodule pointer updated.
- NATS is the sole inter-service transport. Service tokens are defined in
enums/services.enum.ts(e.g.,NATS_SERVICE) and injected via@Inject(NATS_SERVICE). - Prisma schemas differ per service: SQLite for products-ms, PostgreSQL for orders-ms, MongoDB for auth-ms. Run
npx prisma migrate dev(SQL databases) ornpx prisma generate(MongoDB) as needed. - Node version:
20.14.0(pinned in all Dockerfiles withnode:20.14.0-alpine3.20).