VeztaVezta
Guides

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. Major groups:

  • Auth & Waitlist -- waitlist, auth, sub-wallet
  • Core Trading -- market, trading, portfolio, merge-split, sniper, counter-trade, copy-trade, arbitrage, combo (multi-leg parlays), admin-wallet
  • AI & Sports -- agent (in-app AI chat), research, sports-stats, sports-alerts
  • Platform -- leaderboard, tracker, wallet-list, wallet-profile, activity, notification, push-notification, rewards, events, ai-predictions, monitor, insider-signals, telegram-bot, telegram-signals, alerts, user, account, kyc (+ DFlow Proof), search, share-card, app-version, admin, analytics, calendar, feed, help, health, dflow (proxy controller)

Adding a New Module

Create the Module Folder

mkdir -p src/modules/your-feature/dto

Define 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 90+ models organized by domain (see Data Model for the full breakdown). After modifying the schema:

pnpm prisma:generate   # Regenerate Prisma client after schema changes
# NOTE: do NOT run `pnpm prisma:migrate` (prisma migrate dev) locally —
# migrations are authored/applied on the staging/prod VMs via CI/CD.

Financial 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:

  1. Create a processor class with @Processor('queue-name') and @Process('job-name')
  2. Implement OnModuleInit to register repeatable jobs at startup
  3. Clean old repeatable jobs before registering new ones to avoid duplicates

Existing schedulers include: MarketSyncScheduler, ShortDurationSyncProcessor, SignalIngestionScheduler, SpreadScannerScheduler, NewsSyncScheduler, WalletMonitorScheduler, RewardsScheduler, SniperMonitorScheduler, CounterTradeResetScheduler, ClusterRefreshProcessor (insider signals), and the various worker-domain processors.

Worker domain tagging: new schedulers and processors must declare which worker container should run them via backgroundOnly([Processor, Scheduler], '<domain>') (where <domain> is one of traders, market, misc, websocket, rtds, sports, ai, combos). For setInterval-based services and WS connectors, check shouldRunInDomain('<domain>') inside onModuleInit. This ensures only one container consumes each queue. See vezta-be/plans/backend-optimization/phase-4-specialized-workers.md.

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-production-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:generate    # Regenerate Prisma client (do NOT run prisma:migrate locally)
pnpm prisma:studio      # Launch Prisma Studio GUI
pnpm export:openapi     # Export OpenAPI spec to openapi.json
pnpm lint               # ESLint --fix (currently broken — flat-config mismatch; use pnpm build as the gate)

Key Conventions

  • Path alias: @/* maps to src/*
  • Config: configService.get<string>('app.jwt.secret') for runtime values, process.env for bootstrap-time values only
  • Swagger: always add @ApiTags, @ApiOperation, @ApiResponse to controllers -- the frontend generates its API client from this spec
  • Guards: global JwtAuthGuard applied via APP_GUARD. Use @Public() to exempt endpoints

On this page