Actionable Rules for building and operating highly-concurrent, event-driven back-end services with TypeScript/Node.js on Apache Kafka (or any broker with comparable semantics). Covers naming, schema management, observability, testing, security, and performance.
You're tired of untraceable event failures, brittle schema changes, and microservices that mysteriously break in production. Your current event-driven system feels more like event-driven chaos, and every deployment carries the risk of cascading failures across your entire platform.
Most TypeScript developers build event systems that work—until they don't. You're dealing with:
The real problem? You're treating events like afterthoughts instead of the backbone of your distributed system.
These Cursor Rules transform your TypeScript event-driven development into a systematic, observable, and resilient architecture. Instead of fighting fires, you'll be building systems that self-monitor, auto-scale, and gracefully handle failures.
What you get:
// Before: Scattered event handling
async function handleOrder(data: any) {
// Hope the data is valid
await processOrder(data);
}
// After: Type-safe contracts with validation
export interface CheckoutOrderPlacedV1 {
eventId: string;
occurredAt: string; // ISO-8601 UTC
userId: string;
orderId: string;
totalAmount: number;
}
const handleOrderPlaced = async (event: CheckoutOrderPlacedV1) => {
const result = orderSchema.safeParse(event);
if (!result.success) {
return sendToDLQ(event, result.error);
}
// Type-safe processing guaranteed
};
Every event handler automatically includes correlation tracking, metrics, and structured logging:
// Automatic instrumentation built into the rules
@traced('order.processing')
@metered('orders.processed.count')
async function processOrder(event: CheckoutOrderPlacedV1) {
const span = trace.getActiveSpan();
span?.setAttributes({
'event.id': event.eventId,
'order.id': event.orderId,
'user.id': event.userId
});
// Your business logic with full observability
}
// src/events/checkout.order.placed.v2.ts
export interface CheckoutOrderPlacedV2 extends CheckoutOrderPlacedV1 {
shippingAddress?: Address; // Optional field maintains compatibility
currency: string; // Required field with default handling
}
// Contract testing with real Kafka
describe('Order Processing', () => {
it('handles order placed events', async () => {
const testEvent: CheckoutOrderPlacedV1 = {
eventId: uuid(),
occurredAt: new Date().toISOString(),
userId: 'user-123',
orderId: 'order-456',
totalAmount: 99.99
};
await producer.send('checkout.order.placed.v1', testEvent);
// Assert consumer processed correctly
await waitForEventProcessing();
expect(orderService.getOrder('order-456')).toBeDefined();
});
});
Here's how the rules transform a typical order processing workflow:
# Initialize with proper project structure
mkdir order-service && cd order-service
npm init -y
npm install @types/node typescript zod kafkajs opentelemetry
# Rules automatically scaffold:
# /src/events/ - Event contracts
# /src/handlers/ - Consumer logic
# /src/producers/ - Publishing logic
# /src/infrastructure/ - Observability setup
// src/events/checkout.order.placed.ts
import { z } from 'zod';
export const CheckoutOrderPlacedSchema = z.object({
eventId: z.string().uuid(),
occurredAt: z.string().datetime(),
userId: z.string(),
orderId: z.string(),
totalAmount: z.number().positive(),
});
export type CheckoutOrderPlacedV1 = z.infer<typeof CheckoutOrderPlacedSchema>;
// src/handlers/order-placed.handler.ts
export const handleOrderPlaced = async (
event: CheckoutOrderPlacedV1,
context: EventContext
): Promise<Result<void, ProcessingError>> => {
// Idempotency check (automatic)
if (await isAlreadyProcessed(event.eventId)) {
return Ok(undefined);
}
// Business logic with observability
const result = await orderService.createOrder({
id: event.orderId,
userId: event.userId,
amount: event.totalAmount,
});
if (result.isError()) {
// Structured error handling
return Err(new ProcessingError('ORDER_CREATION_FAILED', result.error));
}
// Mark as processed
await markProcessed(event.eventId);
return Ok(undefined);
};
# Automatic CI/CD pipeline integration
- name: Contract Tests
run: npm run test:contracts
- name: Schema Compatibility Check
run: npm run schema:validate
- name: Performance Benchmarks
run: npm run test:performance
# Project setup
npm install --save-dev @cursor/event-driven-rules
npx cursor-rules init --template=event-driven-ts-kafka
# Environment configuration
cp .env.example .env
# Update Kafka brokers, Schema Registry URL
// src/events/user.registered.ts
export interface UserRegisteredV1 {
eventId: string;
occurredAt: string;
userId: string;
email: string;
registrationSource: 'web' | 'mobile' | 'api';
}
The rules automatically generate boilerplate with proper error handling, observability, and idempotency patterns.
Built-in integration with OpenTelemetry, Prometheus metrics, and structured logging means you get production-ready monitoring from day one.
Week One:
Month Three:
Your event-driven architecture transforms from a source of anxiety into a competitive advantage. Stop fighting infrastructure problems and start building features that matter.
The question isn't whether you need better event architecture—it's whether you're ready to stop accepting fragile systems as inevitable.
You are an expert in TypeScript, Node.js, Apache Kafka, AsyncAPI, OpenTelemetry, Docker & Kubernetes, CI/CD, and security best practices for Event-Driven Microservices.
Key Principles
- Events are first-class citizens; treat each event as an immutable fact.
- Decouple producers and consumers via a broker (Kafka/RabbitMQ). Never allow direct service-to-service calls inside the domain core.
- Version everything (schemas, topics, code). Changes must be backward compatible by default.
- Fail fast, observe always: every handler MUST be instrumented with tracing, metrics, and structured logs.
- Idempotency is mandatory—reprocessing an event MUST NOT cause side effects.
- Prefer functional programming patterns; avoid shared mutable state inside handlers.
- Domain language drives naming: `<bounded-context>.<entity>.<verb>` (e.g., `checkout.order.placed`).
- Infrastructure as Code: broker configuration, ACLs, and topic schemas are stored in Git and deployed via CI.
TypeScript
- Target ES2022, use `strict` and `noUncheckedIndexedAccess` in `tsconfig.json`.
- Use `async/await`; never mix callbacks and promises.
- Define event payloads with `interface` and export them from `/src/contracts/{event-name}.ts`.
- Never export default; always use named exports.
- Directory layout:
/src
/events (payload interfaces + validation)
/handlers (Kafka consumer callbacks)
/producers (Kafka producer wrappers)
/infrastructure (broker, tracer, logger, health)
/tests (unit + contract tests)
- eslint-config: `@typescript-eslint`, `eslint-plugin-functional`, `security-node`.
- Example payload interface
```ts
// src/events/checkout.order.placed.ts
export interface CheckoutOrderPlacedV1 {
eventId: string;
occurredAt: string; // ISO-8601 UTC
userId: string;
orderId: string;
totalAmount: number;
}
```
Error Handling and Validation
- Validate payloads on BOTH sides using Zod or Yup. Reject invalid events with `DLQ` (dead-letter queue) semantics.
- Always wrap handler logic in a try/catch; log with correlation id, then decide:
• Retry (transient error) with exponential backoff (max 5 attempts, jitter).
• Dead-letter (permanent error).
- A handler must return early on validation/authorization failure to avoid nested conditions.
- Use `Result<T, E>` pattern (never throw inside domain logic; convert to typed error objects).
- Catch-all process-level handler: `process.on('unhandledRejection')` → alert + shutdown gracefully.
Apache Kafka
- Topic naming: `<domain>.<entity>.<event>` same as event name; append `.v{major}` for breaking schema changes.
- Partitions: choose based on access pattern; prefer `orderId` or other aggregate id for key to guarantee ordering.
- Replication factor ≥ 3 in production clusters.
- Configure Idempotent Producer (`enable.idempotence=true`) and exactly-once semantics where required.
- Use Confluent Schema Registry (Avro/JSON Schema) and reference from AsyncAPI docs.
- Consumer groups: one logical microservice = one consumer group. Scale by adding instances, not by new groups.
- All producers/consumers must adhere to the same serialization format (Avro with specific record or JSON-Schema + AJV).
- Observability: enable `kafka.client-id` + `kafka.group-id` tags in OpenTelemetry exporter.
Additional Sections
Testing
- Unit test each handler with mocked broker using `testcontainers` Kafka.
- Contract tests: publish real event payloads from `/src/events/*.ts` and assert consumer expectations.
- Replay tests: run production topic dump through staging cluster on each schema migration.
- Chaos: periodically pause partitions or inject latency with `kafka-mock` to validate resilience.
Performance & Scalability
- Measure end-to-end latency (produce → commit) with distributed tracing; target P99 < 200 ms (configurable).
- Monitor `lag` and `time lag` per consumer group; set alert threshold of 3× average processing time.
- Use batch consume (`max.poll.records > 1`) where ordering allows.
- Do NOT enable `auto.commit`—commit offsets only after successful processing.
Security
- Enable TLS for all broker connections.
- Use SASL/OAUTHBEARER where possible; fallback to SCRAM.
- Principle of least privilege: producers need `WRITE` on topic; consumers need `READ`.
- Encrypt sensitive fields at rest & in transit (use envelope encryption if storing payloads).
- Sanitize logs: never log PII—hash or mask values via Winston redaction.
Documentation & Governance
- Generate AsyncAPI docs from TypeScript interfaces via `@asyncapi/generator`. Publish to internal portal.
- Each event must include: purpose, schema, version strategy, producers, consumers, SLA.
- Schemas and docs PR-reviewed like code; breaking changes require RFC.
CI/CD & Operations
- Pull request: lint, type-check, unit + contract tests, `docker build --target test`.
- Merge to `main`: build signed Docker image, push to registry, run `helm upgrade` with blue/green.
- Enable gradual rollout with canary consumer group (5% traffic) before full scale.
- Rollback: revert Helm chart, replay missed events from committed offsets.
Common Pitfalls & Remedies
- Over-granular events → noise → avoid by domain modeling workshop.
- Missing idempotency → duplicate side effects → store processed `eventId` in dedup table (e.g., Redis SET).
- Tight coupling via topic/partition keys → future re-partitioning hell → design keys flexibly.