Backend Development
NestJS modules, DTOs, migrations, and job scheduling
The Vezta backend is a NestJS application running on Fastify (not Express) with Prisma ORM, PostgreSQL, and Redis/BullMQ for background jobs. It serves the REST API on port 3001 and provides real-time data via Socket.IO.
Module Pattern
All business modules live in src/modules/<name>/. Each module follows a standard structure:
src/modules/trading/
├── trading.module.ts # NestJS module definition
├── trading.controller.ts # HTTP routes
├── order.service.ts # Business logic
├── dto/ # Request/response DTOs
│ ├── create-order.dto.ts
│ └── order-response.dto.ts
└── order.processor.ts # BullMQ job handler (optional)Modules are registered in app.module.ts under two groups:
- Core -- market, trading, portfolio, merge-split, sniper, counter-trade, arbitrage
- Platform -- leaderboard, tracker, activity, notification, rewards, events, ai-predictions, monitor, alerts, user, account, kyc, copy-trade, search, and more
Adding a New Module
Create the Module Folder
mkdir -p src/modules/your-feature/dtoDefine DTOs
Use class-validator decorators for request validation and @ApiProperty for OpenAPI spec generation:
import { IsString, IsNumber, Min } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class CreateFeatureDto {
@ApiProperty({ description: 'Feature name' })
@IsString()
name: string;
@ApiProperty({ description: 'Value in USD', example: 10.50 })
@IsNumber()
@Min(0)
value: number;
}Create the Controller
Tag with @ApiTags for Swagger grouping. All routes require JWT by default -- use @Public() to exempt endpoints.
import { Controller, Post, Body } from '@nestjs/common';
import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger';
@ApiTags('your-feature')
@ApiBearerAuth('access-token')
@Controller('api/v1/your-feature')
export class YourFeatureController {
constructor(private readonly service: YourFeatureService) {}
@Post()
@ApiOperation({ summary: 'Create a feature' })
create(@Body() dto: CreateFeatureDto) {
return this.service.create(dto);
}
}Register in app.module.ts
Import the module in the imports array of AppModule.
Database (Prisma)
The schema is at prisma/schema.prisma with 39 models organized by domain. After modifying the schema:
pnpm prisma:migrate # Create and apply migration
pnpm prisma:generate # Regenerate Prisma clientFinancial values must use the Decimal type in Prisma -- never JavaScript floats for money. Standard precisions: @db.Decimal(18, 2) for USD amounts, @db.Decimal(10, 6) for prices, @db.Decimal(5, 4) for rates.
The PrismaModule is @Global(), so PrismaService is available everywhere without explicit imports.
Background Jobs (BullMQ)
BullMQ queues are registered per-module via BullModule.registerQueue(). The standard pattern for scheduled jobs:
- Create a processor class with
@Processor('queue-name')and@Process('job-name') - Implement
OnModuleInitto register repeatable jobs at startup - Clean old repeatable jobs before registering new ones to avoid duplicates
Existing schedulers include: MarketSyncScheduler (market data every 60s), SignalIngestionScheduler (news every 5-15min), SpreadScannerScheduler (arbitrage detection), and RewardsScheduler (point calculations).
BullModule.forRoot() uses REDIS_HOST and REDIS_PORT env vars, not REDIS_URL. In Docker, REDIS_HOST must be the container name (e.g., vezta-redis).
Error Handling
All errors must use ApiException for machine-readable error codes:
import { ApiException } from '@/common/exceptions/api.exception';
import { HttpStatus } from '@nestjs/common';
throw new ApiException(
'ORDER_FAILED',
'Insufficient balance to place order',
HttpStatus.BAD_REQUEST,
);The GlobalExceptionFilter normalizes all errors to { statusCode, code, message }. See Error Codes for the full reference.
Dev Commands
pnpm start:dev # Dev server with hot-reload (port 3001)
pnpm build # Compile TypeScript
pnpm test # Jest unit tests
pnpm test:e2e # E2E tests (requires --runInBand)
pnpm prisma:migrate # Run DB migrations
pnpm prisma:generate # Regenerate Prisma client
pnpm prisma:studio # Launch Prisma Studio GUI
pnpm export:openapi # Export OpenAPI spec to openapi.json
pnpm lint # ESLint with auto-fixKey Conventions
- Path alias:
@/*maps tosrc/* - Config:
configService.get<string>('app.jwt.secret')for runtime values,process.envfor bootstrap-time values only - Swagger: always add
@ApiTags,@ApiOperation,@ApiResponseto controllers -- the frontend generates its API client from this spec - Guards: global
JwtAuthGuardapplied viaAPP_GUARD. Use@Public()to exempt endpoints