Skip to main content

Developer Guide

This guide provides a comprehensive overview for developers working on the iHospita HMS project.

Documentation Reading Order

  1. Overview - Understand the system architecture
  2. User Requirements - Plain-language feature requirements
  3. Questionnaire - Confirmed business decisions
  4. Technical Architecture - Implementation details
  5. Infrastructure - DevOps and deployment
  6. UI/UX Design - Design guidelines
  7. 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

DecisionChoiceRationale
ORMPrismaType-safe queries, excellent DX
AuthenticationKeycloakEnterprise-grade, OAuth2/OIDC
API GatewayKongRate limiting, routing, plugins
CachingRedisFast, supports pub/sub
DatabasePostgreSQLReliable, 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

  1. RESTful conventions - Use proper HTTP methods and status codes
  2. Consistent naming - Use camelCase for JSON properties
  3. Pagination - All list endpoints support pagination
  4. Filtering - Use query parameters for filtering
  5. 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
}

Testing Strategy

Unit Tests

describe('PatientService', () => {
let service: PatientService;
let prisma: PrismaService;

beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [
PatientService,
{ provide: PrismaService, useValue: mockPrisma },
],
}).compile();

service = module.get(PatientService);
});

it('should create patient with family membership', async () => {
const result = await service.createPatient(hospitalId, dto);

expect(result.patient).toBeDefined();
expect(result.familyMembership).toBeDefined();
});
});

Integration Tests

describe('Patient API (e2e)', () => {
it('/POST patients', async () => {
const response = await request(app.getHttpServer())
.post('/api/patients')
.set('Authorization', `Bearer ${token}`)
.send(createPatientDto)
.expect(201);

expect(response.body.data.id).toBeDefined();
});
});

Common Patterns

Repository Pattern

@Injectable()
export class PatientRepository {
constructor(private prisma: PrismaService) {}

async findById(hospitalId: string, id: string): Promise<Patient | null> {
return this.prisma.patient.findFirst({
where: { id, hospitalId },
});
}

async create(hospitalId: string, data: CreatePatientData): Promise<Patient> {
return this.prisma.patient.create({
data: { ...data, hospitalId },
});
}
}

Service Layer

@Injectable()
export class PatientService {
constructor(
private patientRepo: PatientRepository,
private familyService: FamilyMembershipService,
private eventEmitter: EventEmitter2,
) {}

async createPatient(hospitalId: string, dto: CreatePatientDto) {
// Business logic
const patient = await this.patientRepo.create(hospitalId, dto);

// Create family membership
const family = await this.familyService.createForPatient(patient);

// Emit event
this.eventEmitter.emit('patient.created', { patient, family });

return { patient, family };
}
}

Debugging Tips

Logging

// Use structured logging
this.logger.log({
message: 'Patient created',
patientId: patient.id,
hospitalId: patient.hospitalId,
action: 'CREATE',
});

Database Queries

# Enable Prisma query logging
DATABASE_URL="postgresql://..." DEBUG="prisma:query"

API Debugging

# Kong logs
docker-compose logs -f kong

# Service logs
docker-compose logs -f hms-service

Useful Resources