This document explains the technical decisions behind choosing TypeScript and a monorepo architecture for this project.
TypeScript transforms your codebase into living documentation. When developers work with your project, their IDE becomes a powerful guide that shows exactly what each function expects, what it returns, and how different parts of the system connect.
// packages/kafka/src/types.ts - Database events are fully typed
export type DatabaseEvent<T> = {
before: T | null;
after: T;
source: {
version: string;
connector: string;
name: string;
ts_ms: number;
// ... 15+ more typed fields
};
op: "c" | "r" | "u" | "d"; // Only valid operations allowed
};
The monorepo structure prevents the classic problem of duplicated interfaces between services. Instead of each service defining its own version of User
, Organization
, or KafkaMessage
, you define them once and share them everywhere.
// packages/service-discovery/src/types.ts
export type Service = (typeof [utils.SERVICES](<http://utils.SERVICES>))[number];
export type ServiceMap = Record<Service, string>;
// apps/internal/analytics/src/types.ts
export interface AnalyticsEvent {
id: string;
type: string;
userId: string;
timestamp: Date;
metadata?: Record<string, any>;
}
export interface KafkaMessage {
topic: string;
partition: number;
offset: string;
value: AnalyticsEvent;
}
// Frontend consuming shared types
import type { Organization, Member } from "@repo/auth";
// Analytics service using the same Kafka types
import type { KafkaMessage, AnalyticsEvent } from "@repo/analytics";
// Service discovery used everywhere
import type { ServiceMap } from "@repo/service-discovery";
Your AnalyticsEvent
interface is defined once in apps/internal/analytics/src/types.ts
and used by both producers and consumers.