| name | typing-generic-responses |
| description | Defines generic types for cross-boundary data: ApiResponse<T>, Paginated<T>, Result<T,E>, Repository<T,ID>. Use when designing HTTP responses, use-case return types, or persistence ports. |
Typing Generic Responses
When to use
- Designing HTTP response shapes
- Defining use-case return types
- Creating persistence ports (Repository interfaces)
Core rules
ApiResponse<T> for all HTTP responses (success + error envelope)
Result<T,E> for use-case outputs (typed success/error without throwing)
Paginated<T> for list endpoint responses
Repository<T,ID> generic interface for persistence ports
- All cross-boundary types are explicit, no
any or object
Reference shape (TypeScript)
ApiResponse
export interface ApiResponse<T = unknown> {
readonly success: boolean;
readonly data?: T;
readonly error?: { code: string; message: string; details?: Record<string, unknown> };
readonly metadata?: { timestamp: string; traceId: string };
}
Result<T,E>
export type Result<T, E extends AppError = AppError> =
| { readonly success: true; readonly value: T }
| { readonly success: false; readonly error: E };
export const ok = <T>(value: T): Result<T, never> => ({ success: true, value });
export const err = <E extends AppError>(error: E): Result<never, E> => ({ success: false, error });
Repository<T,ID>
export interface Repository<T, ID> {
findById(id: ID): Promise<T | null>;
save(entity: T): Promise<T>;
deleteById(id: ID): Promise<boolean>;
}
Examples — Do
async execute(id: string): Promise<Result<User, NotFoundError>> {
const user = await this.repo.findById(id);
return user ? ok(user) : err(new NotFoundError('User', id));
}
Examples — Don't
async findUser(id: string): Promise<any> { ... }
return res.status(200).json({ data: user });
Checklist
See reference/generic-patterns.md for full patterns.