11 KiB
ADR 0001: Implement data-access API Requests
| Field | Value |
|---|---|
| Status | Draft |
| Date | 29.09.2025 |
| Owners | Lorenz, Nino |
| Participants | N/A |
| Related ADRs | N/A |
| Tags | architecture, data-access, library, swagger |
Summary (Decision in One Sentence)
Standardize all data-access libraries with a three-layer architecture: Zod schemas for validation, domain models extending generated DTOs, and services with consistent error handling and logging.
Context & Problem Statement
Inconsistent patterns across data-access libraries (catalogue, remission, crm, oms) cause:
- High cognitive load when switching domains
- Duplicated validation and error handling code
- Mixed approaches to request cancellation and logging
- No standard for extending generated DTOs
Goals: Standardize structure, reduce boilerplate 40%, eliminate validation runtime errors, improve type safety.
Constraints: Must integrate with generated Swagger clients, support AbortSignal, align with @isa/core/logging.
Scope: Schema validation, model extensions, service patterns, standard exports.
Decision
Implement a three-layer architecture for all data-access libraries:
-
Schema Layer (
schemas/): Zod schemas for input validation and type inference- Pattern:
<Operation>Schemawith<Operation>and<Operation>Inputtypes - Example:
SearchItemsSchema,SearchItems,SearchItemsInput
- Pattern:
-
Model Layer (
models/): Domain-specific interfaces extending generated DTOs- Pattern:
interface MyModel extends GeneratedDTO { ... } - Use
EntityContainer<T>for lazy-loaded relationships
- Pattern:
-
Service Layer (
services/): Injectable services integrating Swagger clients- Pattern: Async methods with AbortSignal support
- Standardized error handling with
ResponseArgsError - Structured logging via
@isa/core/logging
Standard exports structure:
// src/index.ts
export * from './lib/models';
export * from './lib/schemas';
export * from './lib/services';
// Optional: stores, helpers
Rationale
Why this approach:
- Type Safety: Zod provides runtime validation + compile-time types with zero manual type definitions
- Separation of Concerns: Clear boundaries between validation, domain logic, and API integration
- Consistency: Identical patterns across all domains reduce cognitive load
- Maintainability: Changes to generated clients don't break domain-specific enhancements
- Developer Experience: Auto-completion, type inference, and standardized error handling improve velocity
Evidence supporting this decision:
- Analysis of 4 existing data-access libraries shows these patterns emerging naturally
RemissionReturnReceiptServicedemonstrates successful integration with loggingEntityContainer<T>pattern proven effective for relationship management- Zod validation catches input errors before API calls, reducing backend load
Consequences
Positive: Consistent patterns, runtime + compile-time type safety, clear maintainability, reusable utilities, structured debugging, optimized performance.
Negative: Migration effort for existing libs, learning curve for Zod, ~1-2ms validation overhead, extra abstraction layer.
Open Questions: User-facing error message conventions, testing standards.
Detailed Design Elements
Schema Validation Pattern
Structure:
// Input validation schema
export const SearchByTermSchema = z.object({
searchTerm: z.string().min(1, 'Search term must not be empty'),
skip: z.number().int().min(0).default(0),
take: z.number().int().min(1).max(100).default(20),
});
// Type inference
export type SearchByTerm = z.infer<typeof SearchByTermSchema>;
export type SearchByTermInput = z.input<typeof SearchByTermSchema>;
Model Extension Pattern
Generated DTO Extension:
import { ProductDTO } from '@generated/swagger/cat-search-api';
export interface Product extends ProductDTO {
name: string;
contributors: string;
catalogProductNumber: string;
// Domain-specific enhancements
}
Entity Container Pattern:
export interface Return extends ReturnDTO {
id: number;
receipts: EntityContainer<Receipt>[]; // Lazy-loaded relationships
}
Service Implementation Pattern
Standard service structure:
@Injectable({ providedIn: 'root' })
export class DomainService {
#apiService = inject(GeneratedApiService);
#logger = logger(() => ({ service: 'DomainService' }));
async fetchData(params: InputType, abortSignal?: AbortSignal): Promise<ResultType> {
const validated = ValidationSchema.parse(params);
let req$ = this.#apiService.operation(validated);
if (abortSignal) {
req$ = req$.pipe(takeUntilAborted(abortSignal));
}
const res = await firstValueFrom(req$);
if (res.error) {
this.#logger.error('Operation failed', new Error(res.message));
throw new ResponseArgsError(res);
}
return res.result as ResultType;
}
}
Error Handling Strategy
- Input Validation: Zod schemas validate and transform inputs
- API Error Handling: Check
res.errorfrom generated clients - Structured Logging: Log errors with context via
@isa/core/logging - Error Propagation: Throw
ResponseArgsErrorfor consistent handling
Concurrency & Cancellation
- AbortSignal Support: All async operations accept optional AbortSignal
- RxJS Integration: Use
takeUntilAbortedoperator for cancellation - Promise Pattern:
firstValueFromprevents subscription memory leaks - Caching:
@InFlightdecorator prevents duplicate concurrent requests
Extension Points
- Custom Decorators:
@Cache,@InFlight,@CacheTimeToLive - Schema Transformations: Zod
.transform()for data normalization - Model Inheritance: Interface extension for domain-specific properties
- Service Composition: Services can depend on other domain services
Code Examples
Complete Data-Access Library Structure
See full examples in existing implementations:
libs/catalogue/data-access- Basic patternslibs/remission/data-access- Advanced with EntityContainerlibs/crm/data-access- Service exampleslibs/oms/data-access- Model extensions
Quick Reference:
// libs/domain/data-access/src/lib/schemas/fetch-items.schema.ts
import { z } from 'zod';
export const FetchItemsSchema = z.object({
categoryId: z.string().min(1),
skip: z.number().int().min(0).default(0),
take: z.number().int().min(1).max(100).default(20),
filters: z.record(z.any()).default({}),
});
export type FetchItems = z.infer<typeof FetchItemsSchema>;
export type FetchItemsInput = z.input<typeof FetchItemsSchema>;
// libs/domain/data-access/src/lib/models/item.ts
import { ItemDTO } from '@generated/swagger/domain-api';
import { EntityContainer } from '@isa/common/data-access';
import { Category } from './category';
export interface Item extends ItemDTO {
id: number;
displayName: string;
category: EntityContainer<Category>;
// Domain-specific enhancements
isAvailable: boolean;
formattedPrice: string;
}
// Service
@Injectable({ providedIn: 'root' })
export class ItemService {
#itemService = inject(GeneratedItemService);
#logger = logger(() => ({ service: 'ItemService' }));
async fetchItems(
params: FetchItemsInput,
abortSignal?: AbortSignal
): Promise<Item[]> {
this.#logger.debug('Fetching items', () => ({ params }));
const { categoryId, skip, take, filters } = FetchItemsSchema.parse(params);
let req$ = this.#itemService.getItems({
categoryId,
queryToken: { skip, take, filter: filters }
});
if (abortSignal) {
req$ = req$.pipe(takeUntilAborted(abortSignal));
}
const res = await firstValueFrom(req$);
if (res.error) {
this.#logger.error('Failed to fetch items', new Error(res.message));
throw new ResponseArgsError(res);
}
this.#logger.info('Successfully fetched items', () => ({
count: res.result?.length || 0
}));
return res.result as Item[];
}
}
// libs/domain/data-access/src/index.ts
export * from './lib/models';
export * from './lib/schemas';
export * from './lib/services';
Usage in Feature Components
// feature component using the data-access library
import { Component, inject, signal } from '@angular/core';
import { ItemService, Item, FetchItemsInput } from '@isa/domain/data-access';
@Component({
selector: 'app-item-list',
template: `
@if (loading()) {
<div>Loading...</div>
} @else {
@for (item of items(); track item.id) {
<div>{{ item.displayName }}</div>
}
}
`
})
export class ItemListComponent {
#itemService = inject(ItemService);
items = signal<Item[]>([]);
loading = signal(false);
async loadItems(categoryId: string) {
this.loading.set(true);
try {
const params: FetchItemsInput = {
categoryId,
take: 50,
filters: { active: true }
};
const items = await this.#itemService.fetchItems(params);
this.items.set(items);
} catch (error) {
console.error('Failed to load items', error);
} finally {
this.loading.set(false);
}
}
}
Migration Pattern for Existing Services
// Before: Direct HTTP client usage
@Injectable()
export class LegacyItemService {
constructor(private http: HttpClient) {}
getItems(categoryId: string): Observable<any> {
return this.http.get(`/api/items?category=${categoryId}`);
}
}
// After: Standardized data-access pattern
@Injectable({ providedIn: 'root' })
export class ItemService {
#itemService = inject(GeneratedItemService);
#logger = logger(() => ({ service: 'ItemService' }));
async fetchItems(params: FetchItemsInput, abortSignal?: AbortSignal): Promise<Item[]> {
const validated = FetchItemsSchema.parse(params);
// ... standard implementation pattern
}
}
Open Questions / Follow-Ups
- Unresolved design clarifications
- Dependent ADRs required
- External approvals needed
Decision Review & Revalidation
When and how this ADR will be re-evaluated (date, trigger conditions, metrics thresholds).
Status Log
| Date | Change | Author |
|---|---|---|
| 2025-10-02 | Condensed for readability | Lorenz, Nino |
| 2025-09-29 | Created (Draft) | Lorenz |
| 2025-09-25 | Analysis completed, comprehensive patterns documented | Lorenz, Nino |
References
Existing Implementation Examples:
libs/catalogue/data-access- Basic schema and service patternslibs/remission/data-access- Advanced patterns with EntityContainer and storeslibs/common/data-access- Shared utilities and response typesgenerated/swagger/- Generated API clients integration
Key Dependencies:
- Zod - Schema validation library
- ng-swagger-gen - OpenAPI client generation
@isa/core/logging- Structured logging infrastructure@isa/common/data-access- Shared utilities and types
Related Documentation:
- ISA Frontend Copilot Instructions - Data-access patterns
- Tech Stack Documentation - Architecture overview
- Code Style Guidelines - TypeScript and Angular patterns
Document updates MUST reference this ADR number in commit messages:
ADR-NNNN:prefix. Keep this document updated through all lifecycle stages.