Developer Guide
This guide provides a comprehensive overview for developers working on the iHospita HMS project.
Documentation Reading Order
- Overview - Understand the system architecture
- User Requirements - Plain-language feature requirements
- Questionnaire - Confirmed business decisions
- Technical Architecture - Implementation details
- Infrastructure - DevOps and deployment
- UI/UX Design - Design guidelines
- User Journey - User workflow understanding
Architecture Overview
Backend Architecture
The backend follows a modular monorepo architecture using NestJS:
server/
├── apps/ # Independent services
│ ├── auth/ # Authentication & Keycloak integration
│ ├── hms/ # Core hospital management
│ ├── crm/ # Customer relationship management
│ ├── payment/ # Financial operations
│ ├── queue/ # Appointment & queue management
│ ├── report/ # Analytics & reporting
│ └── console/ # Platform administration
└── libs/ # Shared libraries
├── prisma/ # Database ORM & migrations
├── common/ # Shared utilities
└── redis/ # Redis integration
Key Design Decisions
| Decision | Choice | Rationale |
|---|---|---|
| ORM | Prisma | Type-safe queries, excellent DX |
| Authentication | Keycloak | Enterprise-grade, OAuth2/OIDC |
| API Gateway | Kong | Rate limiting, routing, plugins |
| Caching | Redis | Fast, supports pub/sub |
| Database | PostgreSQL | Reliable, full-featured |
Code Standards
TypeScript Guidelines
// Use explicit types for function parameters and returns
function calculateDiscount(amount: number, tierLevel: VipTier): number {
// Implementation
}
// Use interfaces for data structures
interface Patient {
id: string;
hospitalId: string;
firstName: string;
lastName: string;
dateOfBirth: Date;
createdAt: Date;
}
// Use enums for fixed sets of values
enum VipTier {
SILVER = 'SILVER',
GOLD = 'GOLD',
PLATINUM = 'PLATINUM',
}
API Design Principles
- RESTful conventions - Use proper HTTP methods and status codes
- Consistent naming - Use camelCase for JSON properties
- Pagination - All list endpoints support pagination
- Filtering - Use query parameters for filtering
- Error handling - Consistent error response format
// Standard API response format
interface ApiResponse<T> {
success: boolean;
data?: T;
error?: {
code: string;
message: string;
details?: Record<string, any>;
};
meta?: {
page: number;
limit: number;
total: number;
};
}
Database Conventions
// Use UUID for primary keys
model Patient {
id String @id @default(uuid())
// Always include audit fields
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
// Foreign keys with explicit naming
hospitalId String @map("hospital_id")
hospital Hospital @relation(fields: [hospitalId], references: [id])
// Use snake_case for database columns
firstName String @map("first_name")
lastName String @map("last_name")
@@map("patients")
@@index([hospitalId])
}
Multi-Tenancy Implementation
Data Isolation
Every database query must include hospital context:
// Service method example
async findPatients(hospitalId: string, filters: PatientFilters) {
return this.prisma.patient.findMany({
where: {
hospitalId, // REQUIRED: Always filter by hospital
...filters,
},
});
}
Hospital Context Middleware
// Middleware extracts hospital from JWT token
@Injectable()
export class HospitalContextMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
const token = this.extractToken(req);
const decoded = this.jwtService.decode(token);
req.hospitalId = decoded.hospital_id;
req.hospitalPrefix = decoded.hospital_prefix;
next();
}
}
Authentication Flow
Keycloak Integration
// Auth guard validates Keycloak tokens
@Injectable()
export class KeycloakAuthGuard implements CanActivate {
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const token = this.extractToken(request);
// Validate with Keycloak
const isValid = await this.keycloakService.validateToken(token);
if (!isValid) {
throw new UnauthorizedException();
}
// Extract user info from token
const decoded = this.jwtService.decode(token);
request.user = {
id: decoded.sub,
email: decoded.email,
roles: decoded.realm_access?.roles || [],
hospitalId: decoded.hospital_id,
};
return true;
}
}
Role-Based Access Control
// Role decorator
@SetMetadata('roles', roles)
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
// Usage in controller
@Post()
@Roles('ADMIN', 'OWNER')
async createEmployee(@Body() dto: CreateEmployeeDto) {
// Only admins and owners can create employees
}