Skip to content

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

  • 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
  • JWT authentication with configurable expiration
  • Password hashing using bcrypt
  • Protected routes with manual authentication middleware
  • Role-based access (extensible)
  • Token validation and error handling
  • PostgreSQL database
  • Prisma ORM for type-safe database access
  • Complex relations between entities
  • Aggregations and statistical queries
  • Transaction support for data integrity
  • Redis caching with TTL configuration
  • Intelligent cache invalidation on data changes
  • Wrap pattern for clean cache implementation
  • Cache monitoring endpoint for debugging
  • Auto-inventory management (expenses create materials automatically)
  • Financial summaries with aggregations
  • Historical data tracking
  • Audit logging for all operations
  • Low-stock alerts for materials
  • 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
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

This example is perfect for:

  • Financial management systems
  • Inventory control applications
  • Business management platforms
  • Accounting software
  • ERP systems
  • Node.js >= 18.0.0 or Bun >= 1.0.0
  • Docker and Docker Compose
  1. Clone the repository:
Terminal window
git clone https://github.com/veloce-ts/examples
cd examples/financial-management-api
  1. Install dependencies:
Terminal window
npm install
# or
bun install
  1. Setup environment:
Terminal window
cp .env.example .env

Edit .env:

# Database
DATABASE_URL="postgresql://garcia_user:garcia_password_2024@localhost:5432/garcia_renovacion"
# Redis
REDIS_HOST="localhost"
REDIS_PORT=6379
REDIS_PASSWORD="garcia_redis_2024"
# JWT
JWT_SECRET="your-super-secret-key-change-this-in-production"
JWT_EXPIRES_IN="24h"
# CORS
CORS_ORIGIN="http://localhost:5173"
  1. Start services (PostgreSQL + Redis):
Terminal window
docker-compose up -d
  1. Run database migrations:
Terminal window
npx prisma migrate dev
npx prisma generate
  1. Create admin user:
Terminal window
npm run create-admin
  1. Start development server:
Terminal window
npm run dev
# or
bun run dev
@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 }
};
}
}
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']
});
@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 };
}

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) };
}
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 controllers
logger.info('Income created', {
incomeId: income.id,
amount: body.amount,
concept: body.concept,
userId: user.id
});
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');
}
}
});
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")
}
  • POST /api/auth/login - User login
  • POST /api/auth/logout - User logout
  • GET /api/auth/profile - Get current user
  • PUT /api/auth/profile - Update profile
  • POST /api/auth/change-password - Change password
  • GET /api/income - List income (paginated)
  • GET /api/income/summary - Financial summary (total, count, average)
  • GET /api/income/history?months=6 - Historical data
  • GET /api/income/:id - Get by ID
  • POST /api/income - Create income
  • PUT /api/income/:id - Update income
  • DELETE /api/income/:id - Delete income
  • GET /api/expenses - List expenses (paginated)
  • GET /api/expenses/summary - Expense summary with top categories
  • GET /api/expenses/history?months=6 - Historical data
  • GET /api/expenses/:id - Get by ID
  • POST /api/expenses - Create expense (auto-creates material if needed)
  • PUT /api/expenses/:id - Update expense
  • DELETE /api/expenses/:id - Delete expense
  • 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 quantity
  • GET /api/materials/:id - Get by ID
  • POST /api/materials - Create material
  • PUT /api/materials/:id - Update material
  • DELETE /api/materials/:id - Delete material
  • GET /api/employees - List employees
  • GET /api/employees/:id - Get by ID
  • POST /api/employees - Create employee
  • PUT /api/employees/:id - Update employee
  • DELETE /api/employees/:id - Delete employee
  • GET /api/checks - List checks
  • GET /api/checks/pending - Pending checks
  • GET /api/checks/summary - Check summary
  • GET /api/checks/stats - Statistics
  • POST /api/checks - Issue check
  • PUT /api/checks/:id - Update check status
  • DELETE /api/checks/:id - Cancel check
  • GET /api/health - Health check (database, Redis, memory)
  • GET /api/debug/redis - Redis monitoring (keys, stats, sample data)
  • GET /api/debug/openapi - OpenAPI configuration info
  • GET / - API info and available endpoints
  • GET /docs - Veloce-TS generated docs
  • GET /api/docs - Custom Swagger UI
  • GET /openapi.json - OpenAPI specification
Terminal window
# 1. Login
TOKEN=$(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 summary
curl -H "Authorization: Bearer $TOKEN" \
http://localhost:3000/api/income/summary
# 3. Create expense with automatic material creation
curl -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 created
curl -H "Authorization: Bearer $TOKEN" \
"http://localhost:3000/api/materials?searchTerm=Portland"
# 5. Get cached material stats
curl -H "Authorization: Bearer $TOKEN" \
http://localhost:3000/api/materials/stats
# 6. Monitor Redis cache
curl http://localhost:3000/api/debug/redis
  1. Open http://localhost:3000/api/docs
  2. Click “Authorize” button
  3. Login through /api/auth/login endpoint
  4. Copy the token from response
  5. Paste as: Bearer YOUR_TOKEN_HERE
  6. Now you can test all endpoints interactively!

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

Logs are stored in logs/ directory:

  • combined.log - All application logs
  • error.log - Only errors
  • exceptions.log - Uncaught exceptions
  • rejections.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 Configuration
NODE_ENV=production
PORT=3000
# Database (use production credentials)
DATABASE_URL=postgresql://user:pass@prod-db:5432/dbname
# Redis (use production credentials)
REDIS_HOST=prod-redis
REDIS_PORT=6379
REDIS_PASSWORD=your-secure-redis-password
# JWT (CHANGE THESE!)
JWT_SECRET=your-super-secure-production-secret-key
JWT_EXPIRES_IN=24h
# CORS
CORS_ORIGIN=https://your-production-domain.com
# Logging
LOG_LEVEL=info
# BCrypt
BCRYPT_ROUNDS=12
FROM oven/bun:1 as base
WORKDIR /app
# Dependencies
COPY package*.json bun.lockb ./
RUN bun install --frozen-lockfile --production
# Source
COPY . .
# Prisma
RUN bunx prisma generate
# Build
RUN bun run build
# Production image
FROM oven/bun:1-slim
WORKDIR /app
COPY --from=base /app/dist ./dist
COPY --from=base /app/node_modules ./node_modules
COPY --from=base /app/prisma ./prisma
COPY --from=base /app/package.json ./
EXPOSE 3000
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s \
CMD curl -f http://localhost:3000/api/health || exit 1
CMD ["bun", "run", "start"]
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:
  • Material stats: 2 minutes TTL
  • Cache invalidation on create/update/delete
  • Pattern-based invalidation for related data
  • Indexes on frequently queried fields
  • Selective field retrieval with select
  • Connection pooling configured
  • Aggregation queries optimized
  • BigInt → Number conversion for JSON compatibility
  • Selective field inclusion
  • Nested relation handling

After studying this example, you’ll understand:

  1. ✅ Building production-ready APIs with Veloce-TS
  2. ✅ Implementing JWT authentication
  3. ✅ Database design and Prisma ORM usage
  4. ✅ Redis caching strategies
  5. ✅ Complex business logic implementation
  6. ✅ Structured logging and monitoring
  7. ✅ Error handling and recovery
  8. ✅ API documentation with OpenAPI
  9. ✅ Docker containerization
  10. ✅ Production deployment strategies

Found a bug or want to improve this example?

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