Case Clinical Architecture Guide
Overview
This guide provides a comprehensive walkthrough of the Case Clinical application's multi-layered architecture, detailing how data flows from the database through various layers to the frontend.
Architecture Layers
1. Database Layer (Prisma)
Location: libs/api/core/data-access/src/prisma/
The database layer uses Prisma ORM with SQL Server as the primary database.
Key Components:
- Schema Definition:
schema.prisma- Defines all database models and relationships - Database Provider: SQL Server with shadow database for migrations
- Generated Client: Prisma Client with binary targets for multiple environments
Example Model:
model FormLayout {
id String @id @default(cuid())
createdAt DateTime? @default(now())
updatedAt DateTime? @default(now())
name String?
config String? @db.Text
type Int @default(0)
// ... additional fields
}
Key Features:
- Automatic CUID generation for IDs
- Timestamp tracking (createdAt, updatedAt)
- Relationship mapping between entities
- Support for complex data types (Text, JSON)
2. NestJS Backend Layer
2.1 Core Data Access Services
Location: libs/api/core/data-access/src/lib/
ApiCoreDataAccessService
- Purpose: Primary Prisma client wrapper
- Extends:
PrismaClient - Features:
- Module lifecycle management (
OnModuleInit,OnModuleDestroy) - Pusher integration for real-time events
- Custom database procedures (e.g.,
getPatientMRN()) - Transaction support with timeout configuration
- Module lifecycle management (
@Injectable()
export class ApiCoreDataAccessService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
constructor() {
super({
transactionOptions: {
maxWait: 15000,
timeout: 300000,
},
})
}
}
ApiCoreSharedService
- Purpose: High-level business operations wrapper
- Features:
- Azure Blob Storage integration
- Document management
- File processing capabilities
- Batch operations support
2.2 Feature-Specific Data Access Pattern
Location: libs/api/{entity}/data-access/
Each domain entity follows a consistent pattern:
Service Structure:
@Injectable()
export class Api{Entity}DataAccessUserService {
constructor(private readonly data: ApiCoreSharedService) {}
async user{Entities}(userId: string, input?: UserList{Entity}Input) {
return this.data.{entity}.findMany({
where: {
AND: [{
name: { contains: input?.name },
}]
},
take: input?.limit,
skip: input?.skip
})
}
}
2.3 GraphQL Resolver Layer
Location: libs/api/{entity}/feature/
Resolver Pattern:
@Resolver()
@UseGuards(GqlAuthGuard)
export class Api{Entity}FeatureUserResolver {
constructor(private readonly service: Api{Entity}DataAccessUserService) {}
@Query(() => [{Entity}], { nullable: true })
user{Entities}(
@CtxUser() user: User,
@Args({ name: 'input', type: () => UserList{Entity}Input, nullable: true }) input?: UserList{Entity}Input,
) {
return this.service.user{Entities}(user.id, input)
}
}
Key Features:
- JWT-based authentication with
GqlAuthGuard - User context injection via
@CtxUser() - Type-safe GraphQL schema generation
- Input validation through DTOs
3. Business Logic Layer
3.1 Core Business Logic Libraries
Location: libs/business-logic/
The business logic layer is organized into specialized modules:
Available Modules:
- actions/: Business action implementations
- common/: Shared utilities and helpers
- configuration/: Application configuration management
- core/: Core business logic services
- error-handling/: Centralized error management
- foundation/: Base classes and interfaces
- http-service/: HTTP communication utilities
- logging/: Structured logging services
- notifications/: Notification management
- rules-engine/: Business rules validation
- validation/: Input validation logic
3.2 Frontend Business Logic Pattern
Location: libs/web/{entity}/shared/
Business Provider Service:
@Injectable({providedIn: 'root'})
export class {Entity}BusinessProviderService extends ServiceBase {
constructor(
logger: LoggingService,
public data: WebCoreDataAccessService,
serviceContext: ServiceContext
) {
super('NotificationService.{Entity}BusinessProviderService', logger, serviceContext)
}
create{Entity}(input: UserCreate{Entity}Input): Observable<{Entity}> {
const action = new Create{Entity}Action(input);
action.Do(this);
return action.response;
}
}
Business Action Pattern:
export class Create{Entity}Action extends {Entity}BusinessActionBase<{Entity}> {
constructor(private input: UserCreate{Entity}Input) {
super('Create{Entity}Action')
}
preValidateAction() {
this.validationContext.addRule(
new Create{Entity}InputIsValidRule(
'InputIsNotNull',
'The input information is not valid.',
this.input,
this.showRuleMessages
)
)
}
performAction() {
this.response = this.businessProvider.data.userCreate{Entity}({ input: this.input }).pipe(
switchMap((result) => of(result.data.created))
)
}
}
Key Features:
- Action-Based Architecture: Each business operation is encapsulated in an action class
- Validation Pipeline: Pre-validation rules before execution
- Service Context: Shared logging and service context
- Observable Responses: RxJS-based reactive programming
4. Frontend State Management Layer (Redux/NgRx)
4.1 Component Store Pattern
Location: libs/web/{entity}/shared/{entity}.store.ts
Store Structure:
export interface {Entity}FeatureState {
errors?: any
loading?: boolean
item?: {Entity}
done: boolean
formName?: string
{entities}: {Entity}[]
searchQuery?: string
paging?: CorePaging
}
@Injectable()
export class Web{Entity}FeatureStore extends ComponentStore<{Entity}FeatureState> {
constructor(
private readonly data: WebCoreDataAccessService,
private readonly router: Router,
private readonly route: ActivatedRoute,
private readonly toast: WebUiToastService,
private readonly formService: FormService,
private readonly {entity}Service: {Entity}Service
) {
super({
loading: false,
{entities}: [],
done: false,
searchQuery: '',
formName: undefined,
paging: {
limit: 10000,
skip: 0,
},
})
}
}
4.2 Data Access Integration
Location: libs/web/auth/data-access/
Web Data Access Service:
@Injectable()
export class WebAuthDataAccessService {
constructor(public readonly data: WebCoreDataAccessService) {}
me() {
return this.data.me()
}
login(input: LoginInput) {
return this.data.login({ input })
}
}
Key Features:
- Component Store: NgRx Component Store for local state management
- Reactive State: RxJS observables for state changes
- Form Integration: Integration with reactive forms
- Navigation: Router integration for state-based navigation
Complete Data Flow
1. Database → NestJS API
Prisma Schema → Prisma Client → ApiCoreDataAccessService → ApiCoreSharedService
2. NestJS API → GraphQL
Data Access Service → Feature Resolver → GraphQL Schema → API Endpoint
3. Business Logic Integration
Frontend Action → Business Provider Service → Validation Rules → Data Access Service
4. Frontend State Management
GraphQL Response → Component Store → Reactive State → UI Components
Key Architectural Patterns
1. Layered Architecture
- Clear separation of concerns across layers
- Dependency injection throughout the stack
- Consistent patterns across all entities
2. Action-Command Pattern
- Business operations encapsulated as actions
- Validation pipeline before execution
- Centralized error handling and logging
3. Repository Pattern
- Data access abstraction through services
- Consistent CRUD operations across entities
- Type-safe database operations
4. Observer Pattern
- RxJS observables for reactive programming
- Event-driven state management
- Real-time updates through Pusher integration
5. Factory Pattern
- Consistent service and component creation
- Dependency injection container management
- Modular architecture with clear boundaries
Development Guidelines
1. Adding New Entities
- Define Prisma model in
schema.prisma - Create data access service in
libs/api/{entity}/data-access/ - Implement GraphQL resolver in
libs/api/{entity}/feature/ - Create business provider service in
libs/web/{entity}/shared/ - Implement component store in
libs/web/{entity}/shared/
2. Business Logic Implementation
- Extend
{Entity}BusinessActionBasefor new actions - Implement validation rules in
preValidateAction() - Execute business logic in
performAction() - Handle errors through centralized error handling
3. State Management
- Define feature state interface
- Extend
ComponentStorewith typed state - Implement selectors for reactive data access
- Create effects for async operations
Best Practices
- Type Safety: Maintain end-to-end TypeScript types
- Validation: Implement validation at multiple layers
- Error Handling: Use centralized error management
- Testing: Unit tests for business logic and integration tests for data flow
- Documentation: Keep architectural documentation updated
- Performance: Use pagination and lazy loading for large datasets
- Security: Implement proper authentication and authorization at all layers