Financial Management API
Financial Management API - Enterprise System
Section titled “Financial Management API - Enterprise System”Financial Management API is a complete, production-ready REST API for enterprise financial control, demonstrating advanced Veloce-TS features with real-world business logic. This example showcases how to build a robust financial system with authentication, caching, audit trails, and automatic inventory management.
:::tip Production Ready This example is designed to be production-ready and includes all the features you’d need in a real enterprise application: caching, logging, monitoring, and comprehensive error handling. :::
🚀 Features Demonstrated
Section titled “🚀 Features Demonstrated”✅ Core Framework Features
Section titled “✅ Core Framework Features”- 7 REST Controllers (Auth, Income, Expense, Material, Employee, Check, Notification)
- Decorator-based routing with
@Controller,@Get,@Post, etc. - Automatic validation with Zod schemas
- OpenAPI/Swagger documentation auto-generation
- Request/Response serialization (BigInt handling)
- Error handling and recovery strategies
✅ Authentication & Security
Section titled “✅ Authentication & Security”- JWT authentication with configurable expiration
- Password hashing using bcrypt
- Protected routes with manual authentication middleware
- Role-based access (extensible)
- Token validation and error handling
✅ Database & ORM
Section titled “✅ Database & ORM”- PostgreSQL database
- Prisma ORM for type-safe database access
- Complex relations between entities
- Aggregations and statistical queries
- Transaction support for data integrity
✅ Performance & Caching
Section titled “✅ Performance & Caching”- Redis caching with TTL configuration
- Intelligent cache invalidation on data changes
- Wrap pattern for clean cache implementation
- Cache monitoring endpoint for debugging
✅ Advanced Business Logic
Section titled “✅ Advanced Business Logic”- Auto-inventory management (expenses create materials automatically)
- Financial summaries with aggregations
- Historical data tracking
- Audit logging for all operations
- Low-stock alerts for materials
✅ Production Features
Section titled “✅ Production Features”- Structured logging with Winston
- Health checks (database, Redis, memory)
- Graceful shutdown handling
- Docker Compose for easy development
- Environment configuration
- Monitoring endpoints for Redis and system status
🏗️ Architecture Overview
Section titled “🏗️ Architecture Overview”financial-api/├── src/│ ├── controllers/ # 7 REST controllers│ │ ├── auth.controller.ts # Authentication & profile│ │ ├── income.controller.ts # Income management│ │ ├── expense.controller.ts # Expense + auto-material creation│ │ ├── material.controller.ts # Inventory management│ │ ├── employee.controller.ts # Employee records│ │ ├── check.controller.ts # Check management│ │ └── notification.controller.ts # Notifications│ ├── schemas/ # Zod validation schemas│ │ ├── auth.schema.ts│ │ ├── income.schema.ts│ │ ├── expense.schema.ts│ │ └── ...│ ├── config/ # Configuration│ │ ├── prisma.ts # Prisma client│ │ └── redis.ts # Redis client│ ├── utils/ # Utilities│ │ ├── cache.ts # Cache service│ │ ├── logger.ts # Winston logger│ │ └── serializer.ts # Data serialization│ ├── middleware/ # Custom middleware│ │ └── auth.ts # Authentication helper│ └── index.ts # Application entry├── prisma/│ └── schema.prisma # Database schema├── scripts/│ └── create-admin-user.ts # Admin setup script├── docker-compose.yml # PostgreSQL + Redis└── public/ └── docs.html # Custom Swagger UI🎯 Real-World Use Cases
Section titled “🎯 Real-World Use Cases”This example is perfect for:
- Financial management systems
- Inventory control applications
- Business management platforms
- Accounting software
- ERP systems
🛠️ Quick Start
Section titled “🛠️ Quick Start”Prerequisites
Section titled “Prerequisites”- Node.js >= 18.0.0 or Bun >= 1.0.0
- Docker and Docker Compose
Installation
Section titled “Installation”- Clone the repository:
git clone https://github.com/veloce-ts/examplescd examples/financial-management-api- Install dependencies:
npm install# orbun install- Setup environment:
cp .env.example .envEdit .env:
# DatabaseDATABASE_URL="postgresql://garcia_user:garcia_password_2024@localhost:5432/garcia_renovacion"
# RedisREDIS_HOST="localhost"REDIS_PORT=6379REDIS_PASSWORD="garcia_redis_2024"
# JWTJWT_SECRET="your-super-secret-key-change-this-in-production"JWT_EXPIRES_IN="24h"
# CORSCORS_ORIGIN="http://localhost:5173"- Start services (PostgreSQL + Redis):
docker-compose up -d- Run database migrations:
npx prisma migrate devnpx prisma generate- Create admin user:
npm run create-admin- Start development server:
npm run dev# orbun run devAccess Points
Section titled “Access Points”- API Base: http://localhost:3000
- Swagger UI: http://localhost:3000/api/docs
- OpenAPI Spec: http://localhost:3000/openapi.json
- Health Check: http://localhost:3000/api/health
- Redis Monitor: http://localhost:3000/api/debug/redis
📚 Key Implementation Examples
Section titled “📚 Key Implementation Examples”1. JWT Authentication
Section titled “1. JWT Authentication”@Controller('/api/auth')export class AuthController {
@Post('/login') async login(@Body(LoginSchema) body: LoginRequest) { // Find user const user = await prisma.user.findUnique({ where: { username: body.username } });
if (!user || !user.isActive) { throw new Error('Invalid credentials'); }
// Verify password const validPassword = await bcrypt.compare( body.password, user.passwordHash );
if (!validPassword) { throw new Error('Invalid credentials'); }
// Generate JWT const token = jwt.sign( { id: user.id, username: user.username, role: user.role }, process.env.JWT_SECRET!, { expiresIn: process.env.JWT_EXPIRES_IN || '24h' } );
// Update last login await prisma.user.update({ where: { id: user.id }, data: { lastLogin: new Date() } });
return { success: true, data: { user, token } }; }}2. Request Validation with Zod
Section titled “2. Request Validation with Zod”export const CreateExpenseSchema = z.object({ concept: z.string().min(1, 'Concept is required'), amount: z.number().positive('Amount must be positive'), category: z.string().min(1, 'Category is required'), paymentMethod: z.string().min(1, 'Payment method is required'), transactionDate: z.string() .refine(val => !isNaN(Date.parse(val)), 'Invalid date') .transform(val => new Date(val)),
// Material purchase fields isMaterialPurchase: z.boolean().default(false), materialName: z.string().optional(), materialQuantity: z.number().positive().optional(), materialUnit: z.string().optional(), materialCode: z.string().optional()}).refine((data) => { // Conditional validation if (data.isMaterialPurchase) { return data.materialName && data.materialQuantity && data.materialUnit; } return true;}, { message: 'Material fields are required when isMaterialPurchase is true', path: ['materialName']});3. Redis Caching Strategy
Section titled “3. Redis Caching Strategy”@Get('/stats')async getMaterialStats(@Header('authorization') authHeader?: string) { const user = await authenticateUser(authHeader);
const cacheKey = cacheService.generateKey('material', 'stats');
const stats = await cacheService.wrap( cacheKey, async () => { // Expensive database operation const [total, active, inactive, aggregates, materials] = await Promise.all([ prisma.material.count(), prisma.material.count({ where: { status: 'activo' } }), prisma.material.count({ where: { status: 'inactivo' } }), prisma.material.aggregate({ _sum: { currentQuantity: true, unitCost: true }, where: { status: 'activo' } }), prisma.material.findMany({ where: { status: 'activo' }, select: { currentQuantity: true, unitCost: true } }) ]);
// Calculate total inventory value const totalValue = materials.reduce((sum, m) => { return sum + (Number(m.currentQuantity) * Number(m.unitCost || 0)); }, 0);
return { total, active, inactive, totalQuantity: Number(aggregates._sum.currentQuantity) || 0, totalValue, lowStock: await prisma.material.count({ where: { status: 'activo', currentQuantity: { lt: 10 } } }) }; }, 120 // Cache for 2 minutes );
return { success: true, data: stats };}4. Auto-Creating Materials from Expenses
Section titled “4. Auto-Creating Materials from Expenses”This demonstrates complex business logic:
@Post('/')async createExpense( @Body(CreateExpenseSchema) body: CreateExpenseRequest, @Header('authorization') authHeader?: string) { const user = await authenticateUser(authHeader); let autoCreatedMaterialId: number | undefined;
// Auto-create or update material if this is a material purchase if (body.isMaterialPurchase && body.materialName && body.materialQuantity) { // Check if material exists let material = await prisma.material.findFirst({ where: { name: body.materialName } });
if (material) { // Update existing material quantity const updated = await prisma.material.update({ where: { id: material.id }, data: { currentQuantity: Number(material.currentQuantity) + Number(body.materialQuantity), unitCost: body.amount // Update price } }); autoCreatedMaterialId = updated.id;
logger.info('Material updated from expense', { materialId: updated.id, addedQuantity: body.materialQuantity }); } else { // Create new material const newMaterial = await prisma.material.create({ data: { code: body.materialCode || `MAT-${Date.now()}`, name: body.materialName, currentQuantity: body.materialQuantity, unitCost: body.amount, unit: body.materialUnit || 'unidad', minQuantity: 0, status: 'activo', userId: user.id } }); autoCreatedMaterialId = newMaterial.id;
logger.info('Material auto-created from expense', { materialId: newMaterial.id, name: newMaterial.name }); } }
// Create expense with material reference const expense = await prisma.expense.create({ data: { ...body, userId: user.id, autoCreatedMaterialId } });
// Audit log await prisma.auditLog.create({ data: { tableName: 'expenses', recordId: expense.id, action: 'CREATE', newData: expense, userId: user.id } });
return { success: true, data: serializeData(expense) };}5. Structured Logging
Section titled “5. Structured Logging”import winston from 'winston';
const logger = winston.createLogger({ level: 'info', format: winston.format.combine( winston.format.timestamp(), winston.format.errors({ stack: true }), winston.format.json() ), defaultMeta: { service: 'garcia-renovacion-api', version: '2.0.0', framework: 'veloce-ts' }, transports: [ new winston.transports.File({ filename: 'logs/error.log', level: 'error' }), new winston.transports.File({ filename: 'logs/combined.log' }) ]});
// Usage in controllerslogger.info('Income created', { incomeId: income.id, amount: body.amount, concept: body.concept, userId: user.id});6. Health Checks
Section titled “6. Health Checks”app.get('/api/health', { handler: async () => { try { // Check PostgreSQL await prisma.$queryRaw`SELECT 1`;
// Check Redis let redisStatus = 'Disconnected'; try { const redis = redisClient.getClient(); if (redis && redisClient.isHealthy()) { await redis.ping(); redisStatus = 'Connected'; } } catch (redisError) { logger.warn('Redis health check failed', { error: redisError.message }); }
return { success: true, data: { status: 'OK', database: 'PostgreSQL - Connected', cache: `Redis - ${redisStatus}`, uptime: process.uptime(), memory: process.memoryUsage(), timestamp: new Date().toISOString() } }; } catch (error) { throw new Error('Health check failed'); } }});📊 Database Schema
Section titled “📊 Database Schema”Key Models
Section titled “Key Models”model User { id Int @id @default(autoincrement()) username String @unique @db.VarChar(50) email String @unique @db.VarChar(255) passwordHash String @map("password_hash") role String @default("user") fullName String? @map("full_name") isActive Boolean @default(true) lastLogin DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt
income Income[] expenses Expense[] materials Material[] employees Employee[] auditLogs AuditLog[]
@@map("users")}
model Income { id Int @id @default(autoincrement()) amount Decimal @db.Decimal(10, 2) concept String @db.VarChar(255) category String @db.VarChar(100) paymentMethod String @map("payment_method") transactionDate DateTime @map("transaction_date") @db.Date status String @default("activo") userId Int @map("user_id")
user User @relation(fields: [userId], references: [id])
@@index([transactionDate]) @@map("income")}
model Expense { id Int @id @default(autoincrement()) amount Decimal @db.Decimal(10, 2) concept String category String? supplier String? paymentMethod String? @map("payment_method") transactionDate DateTime @map("transaction_date") @db.Date status String @default("activo")
// Material auto-creation fields isMaterialPurchase Boolean @default(false) materialName String? @map("material_name") materialQuantity Decimal? @map("material_quantity") materialUnit String? @map("material_unit") materialCode String? @map("material_code") autoCreatedMaterialId Int? @map("auto_created_material_id")
userId Int @map("user_id") user User @relation(fields: [userId], references: [id])
@@index([isMaterialPurchase]) @@map("expenses")}
model Material { id Int @id @default(autoincrement()) code String @unique name String currentQuantity Decimal @default(0) @map("current_quantity") unitCost Decimal? @map("unit_cost") minQuantity Decimal @default(0) @map("min_quantity") unit String? status String @default("activo") userId Int @map("user_id")
user User @relation(fields: [userId], references: [id])
@@map("materials")}🎯 API Endpoints
Section titled “🎯 API Endpoints”Authentication
Section titled “Authentication”POST /api/auth/login- User loginPOST /api/auth/logout- User logoutGET /api/auth/profile- Get current userPUT /api/auth/profile- Update profilePOST /api/auth/change-password- Change password
Income Management
Section titled “Income Management”GET /api/income- List income (paginated)GET /api/income/summary- Financial summary (total, count, average)GET /api/income/history?months=6- Historical dataGET /api/income/:id- Get by IDPOST /api/income- Create incomePUT /api/income/:id- Update incomeDELETE /api/income/:id- Delete income
Expense Management
Section titled “Expense Management”GET /api/expenses- List expenses (paginated)GET /api/expenses/summary- Expense summary with top categoriesGET /api/expenses/history?months=6- Historical dataGET /api/expenses/:id- Get by IDPOST /api/expenses- Create expense (auto-creates material if needed)PUT /api/expenses/:id- Update expenseDELETE /api/expenses/:id- Delete expense
Material/Inventory Management
Section titled “Material/Inventory Management”GET /api/materials- List materials (with filters: category, status, search)GET /api/materials/stats- Inventory statistics (cached)GET /api/materials/low-stock- Materials below minimum quantityGET /api/materials/:id- Get by IDPOST /api/materials- Create materialPUT /api/materials/:id- Update materialDELETE /api/materials/:id- Delete material
Employee Management
Section titled “Employee Management”GET /api/employees- List employeesGET /api/employees/:id- Get by IDPOST /api/employees- Create employeePUT /api/employees/:id- Update employeeDELETE /api/employees/:id- Delete employee
Check Management
Section titled “Check Management”GET /api/checks- List checksGET /api/checks/pending- Pending checksGET /api/checks/summary- Check summaryGET /api/checks/stats- StatisticsPOST /api/checks- Issue checkPUT /api/checks/:id- Update check statusDELETE /api/checks/:id- Cancel check
System & Monitoring
Section titled “System & Monitoring”GET /api/health- Health check (database, Redis, memory)GET /api/debug/redis- Redis monitoring (keys, stats, sample data)GET /api/debug/openapi- OpenAPI configuration infoGET /- API info and available endpointsGET /docs- Veloce-TS generated docsGET /api/docs- Custom Swagger UIGET /openapi.json- OpenAPI specification
🧪 Testing the API
Section titled “🧪 Testing the API”Using cURL
Section titled “Using cURL”# 1. LoginTOKEN=$(curl -s -X POST http://localhost:3000/api/auth/login \ -H "Content-Type: application/json" \ -d '{"username":"admin","password":"admin123"}' \ | jq -r '.data.token')
# 2. Get income summarycurl -H "Authorization: Bearer $TOKEN" \ http://localhost:3000/api/income/summary
# 3. Create expense with automatic material creationcurl -X POST http://localhost:3000/api/expenses \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "amount": 5000, "concept": "Portland Cement Purchase", "category": "materials", "supplier": "ACME Building Supplies", "paymentMethod": "cash", "transactionDate": "2024-11-01", "isMaterialPurchase": true, "materialName": "Portland Cement", "materialQuantity": 50, "materialUnit": "bags", "materialCode": "MAT-CEM-001" }'
# 4. Check that material was createdcurl -H "Authorization: Bearer $TOKEN" \ "http://localhost:3000/api/materials?searchTerm=Portland"
# 5. Get cached material statscurl -H "Authorization: Bearer $TOKEN" \ http://localhost:3000/api/materials/stats
# 6. Monitor Redis cachecurl http://localhost:3000/api/debug/redisUsing Swagger UI
Section titled “Using Swagger UI”- Open http://localhost:3000/api/docs
- Click “Authorize” button
- Login through
/api/auth/loginendpoint - Copy the token from response
- Paste as:
Bearer YOUR_TOKEN_HERE - Now you can test all endpoints interactively!
🔍 Monitoring & Observability
Section titled “🔍 Monitoring & Observability”Redis Monitoring
Section titled “Redis Monitoring”Visit http://localhost:3000/api/debug/redis to see:
- Total keys in cache
- All cache keys
- Connection stats
- Sample data from cache
- Cache patterns used
Application Logs
Section titled “Application Logs”Logs are stored in logs/ directory:
combined.log- All application logserror.log- Only errorsexceptions.log- Uncaught exceptionsrejections.log- Unhandled promise rejections
Example log entry:
{ "level": "info", "message": "Income created", "timestamp": "2024-11-01T00:00:00.000Z", "service": "garcia-renovacion-api", "version": "2.0.0", "framework": "veloce-ts", "incomeId": 1, "amount": 1000, "concept": "Client payment", "userId": 1}🚢 Production Deployment
Section titled “🚢 Production Deployment”Environment Variables
Section titled “Environment Variables”# Production ConfigurationNODE_ENV=productionPORT=3000
# Database (use production credentials)DATABASE_URL=postgresql://user:pass@prod-db:5432/dbname
# Redis (use production credentials)REDIS_HOST=prod-redisREDIS_PORT=6379REDIS_PASSWORD=your-secure-redis-password
# JWT (CHANGE THESE!)JWT_SECRET=your-super-secure-production-secret-keyJWT_EXPIRES_IN=24h
# CORSCORS_ORIGIN=https://your-production-domain.com
# LoggingLOG_LEVEL=info
# BCryptBCRYPT_ROUNDS=12Docker Deployment
Section titled “Docker Deployment”FROM oven/bun:1 as baseWORKDIR /app
# DependenciesCOPY package*.json bun.lockb ./RUN bun install --frozen-lockfile --production
# SourceCOPY . .
# PrismaRUN bunx prisma generate
# BuildRUN bun run build
# Production imageFROM oven/bun:1-slimWORKDIR /app
COPY --from=base /app/dist ./distCOPY --from=base /app/node_modules ./node_modulesCOPY --from=base /app/prisma ./prismaCOPY --from=base /app/package.json ./
EXPOSE 3000
# Health checkHEALTHCHECK --interval=30s --timeout=3s --start-period=10s \ CMD curl -f http://localhost:3000/api/health || exit 1
CMD ["bun", "run", "start"]Docker Compose (Production)
Section titled “Docker Compose (Production)”version: '3.8'
services: api: build: . ports: - "3000:3000" environment: DATABASE_URL: postgresql://user:pass@postgres:5432/db REDIS_HOST: redis depends_on: postgres: condition: service_healthy redis: condition: service_healthy restart: unless-stopped
postgres: image: postgres:16-alpine environment: POSTGRES_DB: production_db POSTGRES_USER: prod_user POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} volumes: - postgres_data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U prod_user"] interval: 10s timeout: 5s retries: 5
redis: image: redis:7-alpine command: redis-server --requirepass ${REDIS_PASSWORD} volumes: - redis_data:/data healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 10s timeout: 3s retries: 5
volumes: postgres_data: redis_data:📈 Performance Considerations
Section titled “📈 Performance Considerations”Caching Strategy
Section titled “Caching Strategy”- Material stats: 2 minutes TTL
- Cache invalidation on create/update/delete
- Pattern-based invalidation for related data
Database Optimization
Section titled “Database Optimization”- Indexes on frequently queried fields
- Selective field retrieval with
select - Connection pooling configured
- Aggregation queries optimized
Response Serialization
Section titled “Response Serialization”- BigInt → Number conversion for JSON compatibility
- Selective field inclusion
- Nested relation handling
🎓 Learning Outcomes
Section titled “🎓 Learning Outcomes”After studying this example, you’ll understand:
- ✅ Building production-ready APIs with Veloce-TS
- ✅ Implementing JWT authentication
- ✅ Database design and Prisma ORM usage
- ✅ Redis caching strategies
- ✅ Complex business logic implementation
- ✅ Structured logging and monitoring
- ✅ Error handling and recovery
- ✅ API documentation with OpenAPI
- ✅ Docker containerization
- ✅ Production deployment strategies
🔗 Additional Resources
Section titled “🔗 Additional Resources”- Veloce-TS Documentation
- Authentication Guide
- Caching Guide
- Prisma Documentation
- Redis Best Practices
🤝 Contributing
Section titled “🤝 Contributing”Found a bug or want to improve this example?
📝 License
Section titled “📝 License”MIT License - Free to use and modify for your projects!
:::tip Ready to Build? This example provides a solid foundation for building enterprise financial systems. Fork it, customize it, and make it your own! :::