Backend Services (@panels/services)
The backend services provide the core API and business logic for the Panels Management System. Built with Fastify, TypeScript, and MikroORM, it delivers high-performance, type-safe REST APIs with comprehensive validation and multi-tenant support.
Overview
Package Name: @panels/services
Location: apps/services/
Framework: Fastify 5.3.2 with TypeScript
Database: PostgreSQL with MikroORM 6.4.16
Port: 3001 (development)
Technology Stack
Core Framework
- Fastify 5.3.2 - High-performance HTTP server
- TypeScript 5.7.2 - Type safety and developer experience
- Node.js 22+ - Runtime environment
Database & ORM
- MikroORM 6.4.16 - TypeScript-first ORM
- PostgreSQL Driver - Database connectivity
- Redis - Caching and session storage
Validation & Documentation
- Zod 3.25.51 - Runtime validation and type generation
- @fastify/swagger - Auto-generated API documentation
- fastify-type-provider-zod - Zod integration for Fastify
Authentication & Security
- @fastify/jwt - JWT token handling
- @fastify/auth - Authentication hooks
- @fastify/cors - Cross-origin resource sharing
- @fastify/helmet - Security headers
Project Structure
apps/services/
├── src/
│ ├── modules/ # Feature modules
│ │ ├── panel/ # Panel management
│ │ │ ├── entities/ # Database entities
│ │ │ ├── routes/ # HTTP route handlers
│ │ │ └── services/ # Business logic
│ │ ├── view/ # View management
│ │ ├── column/ # Column management
│ │ ├── datasource/ # Data source management
│ │ └── change/ # Change tracking
│ ├── database/ # Database configuration
│ │ ├── migrations/ # Database migrations
│ │ └── seeders/ # Test data seeders
│ ├── plugins/ # Fastify plugins
│ ├── utils/ # Utility functions
│ └── app.ts # Application entry point
├── test/ # Test files
├── mikro-orm.config.ts # ORM configuration
├── tsconfig.json # TypeScript configuration
└── package.json # Package configuration
API Architecture
REST API Design
The API follows RESTful principles with consistent patterns:
GET /api/panels # List panels
POST /api/panels # Create panel
GET /api/panels/:id # Get panel
PUT /api/panels/:id # Update panel
DELETE /api/panels/:id # Delete panel
GET /api/panels/:id/datasources # List data sources
POST /api/panels/:id/datasources # Create data source
PUT /api/datasources/:id # Update data source
DELETE /api/datasources/:id # Delete data source
GET /api/panels/:id/columns # List columns
POST /api/panels/:id/columns/base # Create base column
POST /api/panels/:id/columns/calculated # Create calculated column
Module-based Organization
Each feature is organized into self-contained modules:
Panel Module
typescript
// Entity definition
@Entity()
export class Panel {
@PrimaryKey()
id!: number
@Property()
name!: string
@Property({ nullable: true })
description?: string
@Property()
tenantId!: string
@Property()
userId!: string
@OneToOne()
cohortRule!: CohortRule
@OneToMany(() => DataSource, ds => ds.panel)
dataSources = new Collection<DataSource>(this)
@OneToMany(() => BaseColumn, col => col.panel)
baseColumns = new Collection<BaseColumn>(this)
}
Route Handlers
typescript
// Panel routes with Zod validation
export async function panelRoutes(fastify: FastifyInstance) {
// Create panel
fastify.post('/', {
schema: {
body: CreatePanelSchema,
response: { 200: CreatePanelResponseSchema }
}
}, async (request, reply) => {
const panel = await panelService.create(request.body)
return reply.send(panel)
})
// List panels
fastify.get('/', {
schema: {
querystring: ListPanelsQuerySchema,
response: { 200: ListPanelsResponseSchema }
}
}, async (request, reply) => {
const panels = await panelService.findAll(request.query)
return reply.send(panels)
})
}
Database Design
Entity Relationship Model
Panel (1) ←→ (N) DataSource
Panel (1) ←→ (N) BaseColumn
Panel (1) ←→ (N) CalculatedColumn
Panel (1) ←→ (N) View
Panel (1) ←→ (1) CohortRule
View (1) ←→ (N) ViewSort
View (1) ←→ (N) ViewFilter
Panel (1) ←→ (N) PanelChange
View (1) ←→ (N) ViewNotification
Core Entities
Panel Entity
typescript
@Entity()
export class Panel {
@PrimaryKey()
id!: number
@Property()
name!: string
@Property({ nullable: true })
description?: string
@Property()
tenantId!: string
@Property()
userId!: string
@Property()
createdAt = new Date()
@Property({ onUpdate: () => new Date() })
updatedAt = new Date()
@OneToOne(() => CohortRule, { eager: true })
cohortRule!: CohortRule
}
Data Source Entity
typescript
@Entity()
export class DataSource {
@PrimaryKey()
id!: number
@Property()
type!: string
@Property({ type: 'json' })
config!: Record<string, any>
@Property()
lastSyncAt?: Date
@ManyToOne(() => Panel)
panel!: Panel
@Property()
tenantId!: string
@Property()
userId!: string
}
Column Entities
typescript
@Entity()
export class BaseColumn {
@PrimaryKey()
id!: number
@Property()
name!: string
@Property()
type!: string
@Property()
sourceField!: string
@Property({ type: 'json' })
properties!: Record<string, any>
@ManyToOne(() => Panel)
panel!: Panel
@ManyToOne(() => DataSource)
dataSource!: DataSource
}
@Entity()
export class CalculatedColumn {
@PrimaryKey()
id!: number
@Property()
name!: string
@Property()
formula!: string
@Property({ type: 'json' })
dependencies!: string[]
@ManyToOne(() => Panel)
panel!: Panel
}
Business Logic Services
Panel Service
typescript
export class PanelService {
constructor(
private readonly em: EntityManager,
private readonly changeTracker: ChangeTrackingService
) {}
async create(data: CreatePanelRequest): Promise<CreatePanelResponse> {
const panel = new Panel()
panel.name = data.name
panel.description = data.description
panel.tenantId = data.tenantId
panel.userId = data.userId
// Create default cohort rule
const cohortRule = new CohortRule()
cohortRule.conditions = []
cohortRule.logic = 'AND'
panel.cohortRule = cohortRule
await this.em.persistAndFlush([panel, cohortRule])
// Track change
await this.changeTracker.recordChange({
entityType: 'Panel',
entityId: panel.id,
action: 'CREATE',
tenantId: data.tenantId,
userId: data.userId
})
return panel
}
async findAll(query: ListPanelsQuery): Promise<ListPanelsResponse> {
const panels = await this.em.find(Panel, {
tenantId: query.tenantId,
userId: query.userId
}, {
populate: ['cohortRule']
})
return panels
}
}
Data Source Service
typescript
export class DataSourceService {
async create(panelId: string, data: CreateDataSourceRequest): Promise<CreateDataSourceResponse> {
const panel = await this.em.findOneOrFail(Panel, panelId)
const dataSource = new DataSource()
dataSource.type = data.type
dataSource.config = data.config
dataSource.panel = panel
dataSource.tenantId = data.tenantId
dataSource.userId = data.userId
await this.em.persistAndFlush(dataSource)
// Trigger initial sync
await this.syncData(dataSource.id, data.tenantId, data.userId)
return dataSource
}
async syncData(dataSourceId: number, tenantId: string, userId: string): Promise<void> {
const dataSource = await this.em.findOneOrFail(DataSource, dataSourceId)
// Implement sync logic based on data source type
switch (dataSource.type) {
case 'database':
await this.syncDatabaseSource(dataSource)
break
case 'api':
await this.syncApiSource(dataSource)
break
case 'file':
await this.syncFileSource(dataSource)
break
}
dataSource.lastSyncAt = new Date()
await this.em.flush()
}
}
Validation & Type Safety
Zod Schema Definitions
typescript
// Panel schemas
export const CreatePanelSchema = z.object({
name: z.string().min(1).max(255),
description: z.string().optional(),
tenantId: z.string(),
userId: z.string()
})
export const CreatePanelResponseSchema = z.object({
id: z.number(),
name: z.string(),
description: z.string().nullable(),
tenantId: z.string(),
userId: z.string(),
cohortRule: CohortRuleSchema,
createdAt: z.date(),
updatedAt: z.date()
})
// Data source schemas
export const CreateDataSourceSchema = z.object({
type: z.enum(['database', 'api', 'file']),
config: z.record(z.any()),
tenantId: z.string(),
userId: z.string()
})
Type Generation
Types are automatically generated from Zod schemas:
typescript
export type CreatePanelRequest = z.infer<typeof CreatePanelSchema>
export type CreatePanelResponse = z.infer<typeof CreatePanelResponseSchema>
export type CreateDataSourceRequest = z.infer<typeof CreateDataSourceSchema>
Authentication & Authorization
JWT Authentication
typescript
// JWT plugin configuration
await fastify.register(fastifyJwt, {
secret: process.env.JWT_SECRET!,
sign: {
algorithm: 'HS256',
expiresIn: '24h'
}
})
// Authentication hook
fastify.addHook('preHandler', async (request, reply) => {
try {
await request.jwtVerify()
} catch (err) {
reply.send(err)
}
})
Multi-tenant Security
typescript
// Tenant isolation middleware
export async function tenantIsolation(request: FastifyRequest, reply: FastifyReply) {
const userTenant = request.user.tenantId
const requestTenant = request.body.tenantId || request.params.tenantId
if (userTenant !== requestTenant) {
return reply.code(403).send({ error: 'Access denied to tenant' })
}
}
Performance Optimization
Database Optimization
typescript
// Connection pooling
const config: Options = {
type: 'postgresql',
host: process.env.DB_HOST,
port: Number(process.env.DB_PORT),
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
dbName: process.env.DB_NAME,
pool: {
min: 2,
max: 10,
acquireTimeoutMillis: 30000,
createTimeoutMillis: 30000,
destroyTimeoutMillis: 5000,
reapIntervalMillis: 1000,
createRetryIntervalMillis: 200
}
}
// Query optimization with proper indexing
@Index({ properties: ['tenantId', 'userId'] })
@Index({ properties: ['tenantId', 'createdAt'] })
@Entity()
export class Panel { ... }
Caching Strategy
typescript
// Redis caching
export class CacheService {
constructor(private redis: Redis) {}
async get<T>(key: string): Promise<T | null> {
const value = await this.redis.get(key)
return value ? JSON.parse(value) : null
}
async set(key: string, value: any, ttl: number = 3600): Promise<void> {
await this.redis.setex(key, ttl, JSON.stringify(value))
}
async invalidate(pattern: string): Promise<void> {
const keys = await this.redis.keys(pattern)
if (keys.length > 0) {
await this.redis.del(...keys)
}
}
}
Testing Framework
Test Setup
typescript
// Test configuration
import { MikroORM } from '@mikro-orm/core'
import { FastifyInstance } from 'fastify'
import { buildApp } from '../src/app'
describe('Panel API', () => {
let app: FastifyInstance
let orm: MikroORM
beforeAll(async () => {
app = await buildApp()
orm = app.orm
await orm.getSchemaGenerator().createSchema()
})
afterAll(async () => {
await orm.close()
await app.close()
})
beforeEach(async () => {
await orm.getSchemaGenerator().clearDatabase()
})
})
API Testing
typescript
// Panel creation test
it('should create a panel', async () => {
const response = await app.inject({
method: 'POST',
url: '/api/panels',
headers: {
authorization: `Bearer ${authToken}`
},
payload: {
name: 'Test Panel',
description: 'Test Description',
tenantId: 'tenant-123',
userId: 'user-456'
}
})
expect(response.statusCode).toBe(200)
const panel = JSON.parse(response.payload)
expect(panel.name).toBe('Test Panel')
expect(panel.tenantId).toBe('tenant-123')
expect(panel.cohortRule).toBeDefined()
})
Development Workflow
Available Scripts
bash
# Development
pnpm dev # Start development server with watch mode
pnpm build # Build TypeScript to JavaScript
pnpm start # Start production server
# Database
pnpm migration:create # Create new migration
pnpm migration:apply # Apply pending migrations
pnpm schema:fresh # Drop and recreate schema
# Code Quality
pnpm lint # Run ESLint
pnpm typecheck # TypeScript type checking
pnpm format # Format code with Biome
# Testing
pnpm test # Run tests
pnpm test:watch # Run tests in watch mode
pnpm test:coverage # Run tests with coverage
Development Server
bash
# Start development server
cd apps/services
pnpm dev
# Server starts with:
# - Hot reload enabled
# - Auto-compilation of TypeScript
# - Database connection pooling
# - API documentation at /docs
API Documentation
Auto-generated Documentation
typescript
// Swagger/OpenAPI setup
await fastify.register(fastifySwagger, {
openapi: {
openapi: '3.0.0',
info: {
title: 'Panels API',
description: 'API documentation for Panels Management System',
version: '1.0.0'
},
servers: [
{
url: 'http://localhost:3001',
description: 'Development server'
}
]
}
})
await fastify.register(fastifySwaggerUi, {
routePrefix: '/docs',
uiConfig: {
docExpansion: 'list',
deepLinking: false
}
})
Bruno API Collection
The project includes a comprehensive Bruno API testing collection:
api-tests/
├── panels/
│ ├── create-panel.bru
│ ├── list-panels.bru
│ └── update-panel.bru
├── datasources/
│ ├── create-datasource.bru
│ └── sync-datasource.bru
└── environments/
├── development.bru
└── production.bru
Deployment & Production
Environment Configuration
bash
# Production environment variables
NODE_ENV=production
PORT=3001
DATABASE_URL=postgresql://user:pass@host:5432/panels
REDIS_URL=redis://host:6379
JWT_SECRET=your-secure-secret
LOG_LEVEL=info
Docker Configuration
dockerfile
FROM node:22-alpine
WORKDIR /app
# Install dependencies
COPY package.json pnpm-lock.yaml ./
RUN npm install -g pnpm && pnpm install --frozen-lockfile --prod
# Copy built application
COPY dist/ ./dist/
# Set up health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:3001/health || exit 1
EXPOSE 3001
CMD ["node", "dist/app.js"]
Production Optimizations
- Connection pooling for database efficiency
- Response compression with @fastify/compress
- Rate limiting with @fastify/rate-limit
- Security headers with @fastify/helmet
- Request logging with structured logs
- Health checks for monitoring
- Graceful shutdown handling
Monitoring & Observability
Health Checks
typescript
// Health check endpoint
fastify.get('/health', async (request, reply) => {
const dbStatus = await checkDatabaseConnection()
const redisStatus = await checkRedisConnection()
const health = {
status: 'healthy',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
database: dbStatus,
redis: redisStatus
}
return reply.send(health)
})
Structured Logging
typescript
// Logger configuration
const logger = {
level: process.env.LOG_LEVEL || 'info',
transport: {
target: 'pino-pretty',
options: {
colorize: true,
translateTime: 'HH:MM:ss Z',
ignore: 'pid,hostname'
}
}
}
Future Enhancements
Planned Features
- GraphQL API: Alternative query interface
- Real-time Subscriptions: WebSocket support
- Event Sourcing: Event-driven architecture
- Microservices: Service decomposition
- API Versioning: Backward compatibility
Performance Improvements
- Database Sharding: Horizontal scaling
- Read Replicas: Query optimization
- Advanced Caching: Multi-layer caching
- Query Optimization: Automatic query tuning