@girardmedia/bootspring 3.3.2 → 3.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. package/assets/agents/accessibility-auditor.md +39 -0
  2. package/assets/agents/api-designer.md +40 -0
  3. package/assets/agents/auth-implementer.md +64 -0
  4. package/assets/agents/bug-hunter.md +42 -0
  5. package/assets/agents/bundle-analyzer.md +40 -0
  6. package/assets/agents/cache-optimizer.md +55 -0
  7. package/assets/agents/changelog-writer.md +55 -0
  8. package/assets/agents/ci-cd-builder.md +40 -0
  9. package/assets/agents/code-explainer.md +39 -0
  10. package/assets/agents/code-reviewer.md +39 -0
  11. package/assets/agents/cost-optimizer.md +57 -0
  12. package/assets/agents/cron-scheduler.md +51 -0
  13. package/assets/agents/data-seeder.md +56 -0
  14. package/assets/agents/database-architect.md +40 -0
  15. package/assets/agents/dependency-updater.md +40 -0
  16. package/assets/agents/deploy-checker.md +40 -0
  17. package/assets/agents/docker-optimizer.md +40 -0
  18. package/assets/agents/documentation-writer.md +40 -0
  19. package/assets/agents/email-builder.md +55 -0
  20. package/assets/agents/env-setup.md +40 -0
  21. package/assets/agents/error-handler.md +40 -0
  22. package/assets/agents/eslint-fixer.md +46 -0
  23. package/assets/agents/feature-flagger.md +69 -0
  24. package/assets/agents/git-detective.md +39 -0
  25. package/assets/agents/graphql-builder.md +60 -0
  26. package/assets/agents/incident-responder.md +59 -0
  27. package/assets/agents/log-analyzer.md +39 -0
  28. package/assets/agents/migration-planner.md +41 -0
  29. package/assets/agents/monorepo-navigator.md +39 -0
  30. package/assets/agents/nextjs-expert.md +57 -0
  31. package/assets/agents/notification-builder.md +56 -0
  32. package/assets/agents/onboarding-guide.md +39 -0
  33. package/assets/agents/performance-profiler.md +40 -0
  34. package/assets/agents/prisma-expert.md +57 -0
  35. package/assets/agents/rate-limiter.md +58 -0
  36. package/assets/agents/react-expert.md +58 -0
  37. package/assets/agents/refactorer.md +42 -0
  38. package/assets/agents/regex-builder.md +46 -0
  39. package/assets/agents/release-manager.md +40 -0
  40. package/assets/agents/s3-manager.md +58 -0
  41. package/assets/agents/schema-validator.md +40 -0
  42. package/assets/agents/search-builder.md +62 -0
  43. package/assets/agents/security-auditor.md +39 -0
  44. package/assets/agents/sitemap-generator.md +53 -0
  45. package/assets/agents/stripe-integrator.md +59 -0
  46. package/assets/agents/tailwind-expert.md +55 -0
  47. package/assets/agents/tech-debt-tracker.md +39 -0
  48. package/assets/agents/test-writer.md +42 -0
  49. package/assets/agents/type-fixer.md +45 -0
  50. package/assets/agents/webhook-builder.md +54 -0
  51. package/assets/rules/cpp.md +53 -0
  52. package/assets/rules/css.md +52 -0
  53. package/assets/rules/go.md +50 -0
  54. package/assets/rules/html.md +52 -0
  55. package/assets/rules/java.md +51 -0
  56. package/assets/rules/kotlin.md +50 -0
  57. package/assets/rules/php.md +51 -0
  58. package/assets/rules/python.md +51 -0
  59. package/assets/rules/ruby.md +51 -0
  60. package/assets/rules/rust.md +49 -0
  61. package/assets/rules/shell.md +52 -0
  62. package/assets/rules/sql.md +49 -0
  63. package/assets/rules/swift.md +50 -0
  64. package/assets/rules/typescript.md +52 -0
  65. package/assets/rules/yaml-json.md +51 -0
  66. package/assets/skills/accessibility.md +210 -0
  67. package/assets/skills/agent-patterns.md +387 -0
  68. package/assets/skills/ai-integration.md +263 -0
  69. package/assets/skills/animation-patterns.md +224 -0
  70. package/assets/skills/api-design.md +218 -0
  71. package/assets/skills/api-gateway.md +341 -0
  72. package/assets/skills/api-versioning.md +226 -0
  73. package/assets/skills/astro-patterns.md +233 -0
  74. package/assets/skills/auth-patterns.md +248 -0
  75. package/assets/skills/aws-patterns.md +171 -0
  76. package/assets/skills/background-jobs.md +162 -0
  77. package/assets/skills/browser-extensions.md +309 -0
  78. package/assets/skills/caching-patterns.md +253 -0
  79. package/assets/skills/ci-cd.md +251 -0
  80. package/assets/skills/cli-development.md +296 -0
  81. package/assets/skills/code-review.md +185 -0
  82. package/assets/skills/cron-patterns.md +327 -0
  83. package/assets/skills/data-fetching.md +231 -0
  84. package/assets/skills/database-migrations.md +346 -0
  85. package/assets/skills/database-patterns.md +219 -0
  86. package/assets/skills/debugging.md +281 -0
  87. package/assets/skills/design-system.md +289 -0
  88. package/assets/skills/django-patterns.md +182 -0
  89. package/assets/skills/docker-patterns.md +235 -0
  90. package/assets/skills/e2e-testing.md +287 -0
  91. package/assets/skills/edge-computing.md +268 -0
  92. package/assets/skills/electron-patterns.md +266 -0
  93. package/assets/skills/email-templates.md +206 -0
  94. package/assets/skills/error-handling.md +265 -0
  95. package/assets/skills/event-driven.md +232 -0
  96. package/assets/skills/express-patterns.md +239 -0
  97. package/assets/skills/fastapi-patterns.md +198 -0
  98. package/assets/skills/feature-flags.md +212 -0
  99. package/assets/skills/figma-to-code.md +298 -0
  100. package/assets/skills/file-upload.md +228 -0
  101. package/assets/skills/forms-patterns.md +264 -0
  102. package/assets/skills/gcp-patterns.md +189 -0
  103. package/assets/skills/git-workflow.md +187 -0
  104. package/assets/skills/golang-patterns.md +185 -0
  105. package/assets/skills/graphql-patterns.md +244 -0
  106. package/assets/skills/i18n-patterns.md +172 -0
  107. package/assets/skills/image-processing.md +350 -0
  108. package/assets/skills/java-springboot.md +226 -0
  109. package/assets/skills/kotlin-patterns.md +207 -0
  110. package/assets/skills/kubernetes-patterns.md +326 -0
  111. package/assets/skills/laravel-patterns.md +261 -0
  112. package/assets/skills/llm-fine-tuning.md +335 -0
  113. package/assets/skills/load-testing.md +303 -0
  114. package/assets/skills/logging-observability.md +228 -0
  115. package/assets/skills/markdown-processing.md +318 -0
  116. package/assets/skills/mcp-server-patterns.md +292 -0
  117. package/assets/skills/microservices.md +272 -0
  118. package/assets/skills/migration-patterns.md +239 -0
  119. package/assets/skills/mongodb-patterns.md +189 -0
  120. package/assets/skills/monorepo-patterns.md +287 -0
  121. package/assets/skills/nextjs-app-router.md +237 -0
  122. package/assets/skills/notification-patterns.md +348 -0
  123. package/assets/skills/oauth-patterns.md +246 -0
  124. package/assets/skills/payment-integration.md +222 -0
  125. package/assets/skills/pdf-generation.md +307 -0
  126. package/assets/skills/performance-optimization.md +277 -0
  127. package/assets/skills/php-patterns.md +210 -0
  128. package/assets/skills/prisma-patterns.md +241 -0
  129. package/assets/skills/prompt-engineering.md +193 -0
  130. package/assets/skills/pwa-patterns.md +247 -0
  131. package/assets/skills/python-patterns.md +158 -0
  132. package/assets/skills/python-testing.md +172 -0
  133. package/assets/skills/queue-patterns.md +295 -0
  134. package/assets/skills/rag-patterns.md +159 -0
  135. package/assets/skills/rate-limiting.md +319 -0
  136. package/assets/skills/react-components.md +201 -0
  137. package/assets/skills/react-native-patterns.md +299 -0
  138. package/assets/skills/real-time-patterns.md +181 -0
  139. package/assets/skills/redis-patterns.md +188 -0
  140. package/assets/skills/refactoring.md +218 -0
  141. package/assets/skills/regex-patterns.md +191 -0
  142. package/assets/skills/remix-patterns.md +262 -0
  143. package/assets/skills/responsive-design.md +199 -0
  144. package/assets/skills/ruby-rails-patterns.md +178 -0
  145. package/assets/skills/rust-patterns.md +211 -0
  146. package/assets/skills/search-patterns.md +227 -0
  147. package/assets/skills/security-hardening.md +237 -0
  148. package/assets/skills/seo-patterns.md +179 -0
  149. package/assets/skills/serverless-patterns.md +223 -0
  150. package/assets/skills/sql-optimization.md +154 -0
  151. package/assets/skills/state-management.md +254 -0
  152. package/assets/skills/storybook-patterns.md +330 -0
  153. package/assets/skills/svelte-patterns.md +258 -0
  154. package/assets/skills/swift-patterns.md +227 -0
  155. package/assets/skills/tailwind-patterns.md +272 -0
  156. package/assets/skills/tdd-workflow.md +199 -0
  157. package/assets/skills/terraform-patterns.md +270 -0
  158. package/assets/skills/testing-react.md +240 -0
  159. package/assets/skills/testing-vitest.md +232 -0
  160. package/assets/skills/typescript-strict.md +159 -0
  161. package/assets/skills/video-processing.md +340 -0
  162. package/assets/skills/vue-patterns.md +247 -0
  163. package/assets/skills/web-workers.md +327 -0
  164. package/assets/skills/webhooks-patterns.md +283 -0
  165. package/assets/skills/websocket-patterns.md +306 -0
  166. package/dist/cli/index.js +941 -958
  167. package/dist/core/index.d.ts +341 -11
  168. package/dist/core.js +58 -95
  169. package/dist/mcp/index.d.ts +33 -1
  170. package/dist/mcp-server.js +177 -255
  171. package/package.json +4 -1
@@ -0,0 +1,206 @@
1
+ ---
2
+ name: email-templates
3
+ description: Email template patterns with MJML, inline styles, responsive emails, preview text, and testing across clients.
4
+ ---
5
+
6
+ # Email Templates
7
+
8
+ ## When to Use
9
+ Apply when building transactional or marketing emails. Email HTML is stuck in 2005 -- Outlook uses Word's rendering engine, Gmail strips `<style>` tags, and every client has different quirks. Use MJML to write maintainable markup that compiles to battle-tested HTML. Test in Litmus or Email on Acid before sending.
10
+
11
+ ## How It Works
12
+
13
+ ### MJML -- Write Emails Without Suffering
14
+
15
+ MJML compiles to responsive, cross-client HTML:
16
+
17
+ ```xml
18
+ <mjml>
19
+ <mj-head>
20
+ <mj-attributes>
21
+ <mj-all font-family="Helvetica, Arial, sans-serif" />
22
+ <mj-text font-size="16px" line-height="1.5" color="#374151" />
23
+ </mj-attributes>
24
+ <mj-preview>Your order #1234 has shipped</mj-preview>
25
+ </mj-head>
26
+ <mj-body background-color="#f9fafb">
27
+ <mj-section background-color="#ffffff" border-radius="8px" padding="32px">
28
+ <mj-column>
29
+ <mj-image src="https://example.com/logo.png" width="120px" alt="Logo" />
30
+ <mj-text font-size="24px" font-weight="700" padding-top="24px">
31
+ Your order has shipped!
32
+ </mj-text>
33
+ <mj-text>
34
+ Hi {{name}}, your order #{{orderId}} is on its way.
35
+ Expected delivery: {{deliveryDate}}.
36
+ </mj-text>
37
+ <mj-button background-color="#2563eb" color="#ffffff"
38
+ href="{{trackingUrl}}" border-radius="6px" font-size="16px">
39
+ Track Your Package
40
+ </mj-button>
41
+ </mj-column>
42
+ </mj-section>
43
+ <mj-section padding="16px">
44
+ <mj-column>
45
+ <mj-text font-size="12px" color="#9ca3af" align="center">
46
+ You received this because you placed an order at Example Store.
47
+ <a href="{{unsubscribeUrl}}">Unsubscribe</a>
48
+ </mj-text>
49
+ </mj-column>
50
+ </mj-section>
51
+ </mj-body>
52
+ </mjml>
53
+ ```
54
+
55
+ Compile with: `npx mjml input.mjml -o output.html`
56
+
57
+ ### React Email -- Component-Based Templates
58
+
59
+ ```tsx
60
+ import {
61
+ Body, Container, Head, Heading, Html,
62
+ Preview, Section, Text, Button, Img,
63
+ } from "@react-email/components";
64
+
65
+ interface OrderShippedProps {
66
+ name: string;
67
+ orderId: string;
68
+ trackingUrl: string;
69
+ }
70
+
71
+ export function OrderShipped({ name, orderId, trackingUrl }: OrderShippedProps) {
72
+ return (
73
+ <Html>
74
+ <Head />
75
+ <Preview>Your order #{orderId} has shipped</Preview>
76
+ <Body style={{ backgroundColor: "#f9fafb", fontFamily: "Helvetica, Arial, sans-serif" }}>
77
+ <Container style={{ maxWidth: 600, margin: "0 auto", padding: 32 }}>
78
+ <Img src="https://example.com/logo.png" width={120} alt="Logo" />
79
+ <Heading style={{ fontSize: 24, marginTop: 24 }}>
80
+ Your order has shipped!
81
+ </Heading>
82
+ <Text style={{ fontSize: 16, lineHeight: 1.5, color: "#374151" }}>
83
+ Hi {name}, your order #{orderId} is on its way.
84
+ </Text>
85
+ <Section style={{ textAlign: "center", marginTop: 24 }}>
86
+ <Button
87
+ href={trackingUrl}
88
+ style={{
89
+ backgroundColor: "#2563eb",
90
+ color: "#ffffff",
91
+ padding: "12px 24px",
92
+ borderRadius: 6,
93
+ fontSize: 16,
94
+ textDecoration: "none",
95
+ }}
96
+ >
97
+ Track Your Package
98
+ </Button>
99
+ </Section>
100
+ </Container>
101
+ </Body>
102
+ </Html>
103
+ );
104
+ }
105
+ ```
106
+
107
+ ### Sending with Nodemailer + Template Rendering
108
+
109
+ ```typescript
110
+ import nodemailer from "nodemailer";
111
+ import mjml2html from "mjml";
112
+ import Handlebars from "handlebars";
113
+ import { readFileSync } from "fs";
114
+
115
+ const transporter = nodemailer.createTransport({
116
+ host: process.env.SMTP_HOST,
117
+ port: 587,
118
+ auth: { user: process.env.SMTP_USER, pass: process.env.SMTP_PASS },
119
+ });
120
+
121
+ async function sendOrderShipped(to: string, data: OrderData) {
122
+ const mjmlTemplate = readFileSync("templates/order-shipped.mjml", "utf-8");
123
+ const compiled = Handlebars.compile(mjmlTemplate);
124
+ const mjmlOutput = compiled(data);
125
+ const { html } = mjml2html(mjmlOutput);
126
+
127
+ await transporter.sendMail({
128
+ from: '"Example Store" <orders@example.com>',
129
+ to,
130
+ subject: `Your order #${data.orderId} has shipped`,
131
+ html,
132
+ text: `Hi ${data.name}, your order #${data.orderId} has shipped. Track it: ${data.trackingUrl}`,
133
+ });
134
+ }
135
+ ```
136
+
137
+ ### Preview Text Hack
138
+
139
+ The preview text appears in the inbox list. Control it, or clients will grab the first visible text:
140
+
141
+ ```html
142
+ <!-- Preview text visible in inbox, hidden in email body -->
143
+ <div style="display: none; max-height: 0; overflow: hidden;">
144
+ Your order #1234 has shipped -- arriving Thursday.
145
+ <!-- Pad with whitespace to prevent body text from appearing -->
146
+ &nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;
147
+ </div>
148
+ ```
149
+
150
+ ### Dark Mode Support
151
+
152
+ ```html
153
+ <style>
154
+ @media (prefers-color-scheme: dark) {
155
+ .email-body { background-color: #1f2937 !important; }
156
+ .email-text { color: #f3f4f6 !important; }
157
+ .email-card { background-color: #374151 !important; }
158
+ }
159
+ </style>
160
+ <!-- Fallback for clients that strip style tags -->
161
+ <meta name="color-scheme" content="light dark" />
162
+ <meta name="supported-color-schemes" content="light dark" />
163
+ ```
164
+
165
+ ### Testing Strategy
166
+
167
+ ```typescript
168
+ import { render } from "@react-email/render";
169
+
170
+ describe("OrderShipped email", () => {
171
+ it("renders with required props", () => {
172
+ const html = render(
173
+ <OrderShipped name="Alice" orderId="1234" trackingUrl="https://track.example.com/1234" />
174
+ );
175
+ expect(html).toContain("Your order has shipped");
176
+ expect(html).toContain("Alice");
177
+ expect(html).toContain("track.example.com/1234");
178
+ });
179
+
180
+ it("includes unsubscribe link", () => {
181
+ const html = render(<OrderShipped {...defaultProps} />);
182
+ expect(html).toContain("Unsubscribe");
183
+ });
184
+ });
185
+ ```
186
+
187
+ ## Examples
188
+
189
+ | Pattern | When | Result |
190
+ |---------|------|--------|
191
+ | MJML templates | Marketing emails, newsletters | Cross-client responsive |
192
+ | React Email | Transactional, typed props | Component reuse, TypeScript |
193
+ | Preview text | All emails | Control inbox preview line |
194
+ | Dark mode meta | Modern clients | Respects user preference |
195
+ | Plain text fallback | Accessibility, spam filters | Better deliverability |
196
+ | Litmus/Email on Acid | Pre-send QA | Catch rendering bugs |
197
+
198
+ ## Checklist
199
+ - [ ] Templates use MJML or React Email, not hand-written table HTML
200
+ - [ ] Every email has a plain text fallback
201
+ - [ ] Preview text is explicitly set, not auto-generated from body
202
+ - [ ] CTA buttons use bulletproof button technique (not just `<a>`)
203
+ - [ ] Images have alt text and explicit width/height
204
+ - [ ] Unsubscribe link present in every marketing email (CAN-SPAM)
205
+ - [ ] Dark mode handled with `prefers-color-scheme` and meta tags
206
+ - [ ] Tested in Gmail, Outlook, Apple Mail, and Yahoo before launch
@@ -0,0 +1,265 @@
1
+ ---
2
+ name: error-handling
3
+ description: Handle errors gracefully with Result types, retry logic, circuit breakers, and structured logging.
4
+ ---
5
+
6
+ # Error Handling Patterns
7
+
8
+ ## When to Use
9
+
10
+ Apply these patterns in every service, API, and CLI tool. The default — throwing
11
+ exceptions and hoping a catch block somewhere handles them — leads to crashed
12
+ processes, lost data, and unreadable logs. Explicit error handling is not
13
+ overhead; it's how reliable software works.
14
+
15
+ ## How It Works
16
+
17
+ ### 1. Result Types — Errors as Values
18
+
19
+ Make errors part of the return type so callers can't ignore them.
20
+
21
+ ```typescript
22
+ type Result<T, E = Error> =
23
+ | { ok: true; value: T }
24
+ | { ok: false; error: E };
25
+
26
+ function ok<T>(value: T): Result<T, never> {
27
+ return { ok: true, value };
28
+ }
29
+
30
+ function err<E>(error: E): Result<never, E> {
31
+ return { ok: false, error };
32
+ }
33
+ ```
34
+
35
+ Usage:
36
+
37
+ ```typescript
38
+ function parseConfig(raw: string): Result<Config, ParseError> {
39
+ try {
40
+ const data = JSON.parse(raw);
41
+ if (!data.apiUrl) return err(new ParseError('Missing apiUrl'));
42
+ return ok(data as Config);
43
+ } catch {
44
+ return err(new ParseError('Invalid JSON'));
45
+ }
46
+ }
47
+
48
+ const result = parseConfig(input);
49
+ if (!result.ok) {
50
+ logger.error('Config parse failed', { error: result.error.message });
51
+ process.exit(1);
52
+ }
53
+ // result.value is narrowed to Config here
54
+ ```
55
+
56
+ ### 2. Custom Error Classes
57
+
58
+ Categorize errors so handlers can react differently to different failures.
59
+
60
+ ```typescript
61
+ class AppError extends Error {
62
+ constructor(
63
+ message: string,
64
+ public readonly code: string,
65
+ public readonly statusCode: number = 500,
66
+ public readonly isOperational: boolean = true,
67
+ ) {
68
+ super(message);
69
+ this.name = this.constructor.name;
70
+ }
71
+ }
72
+
73
+ class NotFoundError extends AppError {
74
+ constructor(resource: string, id: string) {
75
+ super(`${resource} ${id} not found`, 'NOT_FOUND', 404);
76
+ }
77
+ }
78
+
79
+ class ValidationError extends AppError {
80
+ constructor(public readonly fields: Record<string, string>) {
81
+ super('Validation failed', 'VALIDATION_ERROR', 400);
82
+ }
83
+ }
84
+
85
+ class ExternalServiceError extends AppError {
86
+ constructor(service: string, cause?: Error) {
87
+ super(`${service} unavailable`, 'EXTERNAL_SERVICE_ERROR', 502, true);
88
+ this.cause = cause;
89
+ }
90
+ }
91
+ ```
92
+
93
+ ### 3. Error Boundaries in Express
94
+
95
+ Centralize error handling in one middleware.
96
+
97
+ ```typescript
98
+ // Must be registered last, with 4 parameters
99
+ app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
100
+ if (err instanceof AppError && err.isOperational) {
101
+ logger.warn('Operational error', {
102
+ code: err.code,
103
+ message: err.message,
104
+ path: req.path,
105
+ });
106
+ return res.status(err.statusCode).json({
107
+ error: { code: err.code, message: err.message },
108
+ });
109
+ }
110
+
111
+ // Unexpected errors — log full stack, return generic message
112
+ logger.error('Unhandled error', {
113
+ error: err.message,
114
+ stack: err.stack,
115
+ path: req.path,
116
+ });
117
+ res.status(500).json({
118
+ error: { code: 'INTERNAL_ERROR', message: 'An unexpected error occurred' },
119
+ });
120
+ });
121
+ ```
122
+
123
+ ### 4. Retry with Exponential Backoff
124
+
125
+ Retry transient failures (network timeouts, rate limits) with increasing delays.
126
+
127
+ ```typescript
128
+ async function retry<T>(
129
+ fn: () => Promise<T>,
130
+ options: { maxAttempts?: number; baseDelayMs?: number; maxDelayMs?: number } = {},
131
+ ): Promise<T> {
132
+ const { maxAttempts = 3, baseDelayMs = 200, maxDelayMs = 10000 } = options;
133
+
134
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
135
+ try {
136
+ return await fn();
137
+ } catch (error) {
138
+ if (attempt === maxAttempts) throw error;
139
+
140
+ const delay = Math.min(
141
+ baseDelayMs * Math.pow(2, attempt - 1) + Math.random() * 100,
142
+ maxDelayMs,
143
+ );
144
+ await new Promise((resolve) => setTimeout(resolve, delay));
145
+ }
146
+ }
147
+ throw new Error('Unreachable');
148
+ }
149
+
150
+ // Usage
151
+ const data = await retry(() => fetchFromAPI('/users'), { maxAttempts: 3 });
152
+ ```
153
+
154
+ Add jitter (the `Math.random()` part) to prevent thundering herds.
155
+
156
+ ### 5. Circuit Breaker
157
+
158
+ Stop calling a failing service to let it recover.
159
+
160
+ ```typescript
161
+ class CircuitBreaker {
162
+ private failures = 0;
163
+ private lastFailure = 0;
164
+ private state: 'closed' | 'open' | 'half-open' = 'closed';
165
+
166
+ constructor(
167
+ private readonly threshold: number = 5,
168
+ private readonly resetTimeMs: number = 30000,
169
+ ) {}
170
+
171
+ async call<T>(fn: () => Promise<T>): Promise<T> {
172
+ if (this.state === 'open') {
173
+ if (Date.now() - this.lastFailure > this.resetTimeMs) {
174
+ this.state = 'half-open';
175
+ } else {
176
+ throw new Error('Circuit breaker is open');
177
+ }
178
+ }
179
+
180
+ try {
181
+ const result = await fn();
182
+ this.onSuccess();
183
+ return result;
184
+ } catch (error) {
185
+ this.onFailure();
186
+ throw error;
187
+ }
188
+ }
189
+
190
+ private onSuccess() {
191
+ this.failures = 0;
192
+ this.state = 'closed';
193
+ }
194
+
195
+ private onFailure() {
196
+ this.failures++;
197
+ this.lastFailure = Date.now();
198
+ if (this.failures >= this.threshold) {
199
+ this.state = 'open';
200
+ }
201
+ }
202
+ }
203
+
204
+ const paymentBreaker = new CircuitBreaker(5, 30000);
205
+ const charge = await paymentBreaker.call(() => stripe.charges.create(params));
206
+ ```
207
+
208
+ ### 6. Structured Logging
209
+
210
+ Log machine-parseable JSON, not random strings.
211
+
212
+ ```typescript
213
+ // Bad
214
+ console.log(`User ${userId} failed to login: ${error.message}`);
215
+
216
+ // Good
217
+ logger.warn('Login failed', {
218
+ userId,
219
+ error: error.message,
220
+ code: error.code,
221
+ ip: req.ip,
222
+ timestamp: new Date().toISOString(),
223
+ });
224
+ ```
225
+
226
+ Use log levels consistently:
227
+ | Level | Use |
228
+ |-------|-----|
229
+ | `error` | Unexpected failures requiring investigation |
230
+ | `warn` | Expected failures (auth denied, validation) |
231
+ | `info` | Significant business events (order created, deploy started) |
232
+ | `debug` | Developer troubleshooting details (only in dev) |
233
+
234
+ ### 7. Graceful Shutdown
235
+
236
+ ```typescript
237
+ process.on('SIGTERM', async () => {
238
+ logger.info('SIGTERM received, shutting down gracefully');
239
+ server.close();
240
+ await db.$disconnect();
241
+ await cache.quit();
242
+ process.exit(0);
243
+ });
244
+ ```
245
+
246
+ ## Examples
247
+
248
+ | Failure type | Pattern | Recovery |
249
+ |-------------|---------|----------|
250
+ | Invalid input | Validation error + 400 | Immediate feedback to caller |
251
+ | Missing resource | Not found error + 404 | Caller adjusts request |
252
+ | Network timeout | Retry with backoff | Auto-recover after delay |
253
+ | Persistent outage | Circuit breaker | Fail fast, serve cached/degraded |
254
+ | Unknown crash | Global error handler | Log, alert, restart |
255
+
256
+ ## Checklist
257
+
258
+ - [ ] Functions that can fail return `Result<T, E>` or throw typed errors, never raw strings
259
+ - [ ] Custom error classes include `code`, `statusCode`, and `isOperational`
260
+ - [ ] Express has a centralized error middleware registered last
261
+ - [ ] External calls use retry with exponential backoff and jitter
262
+ - [ ] Critical dependencies have circuit breakers
263
+ - [ ] All log output is structured JSON with consistent fields
264
+ - [ ] Graceful shutdown handlers close connections before exiting
265
+ - [ ] Production errors never leak stack traces or internal details to clients
@@ -0,0 +1,232 @@
1
+ ---
2
+ name: event-driven
3
+ description: Event-driven architecture with pub/sub, event bus, CQRS, event sourcing, and dead letter queues.
4
+ ---
5
+
6
+ # Event-Driven Architecture
7
+
8
+ ## When to Use
9
+ Apply when services need to communicate without tight coupling, when you need audit trails of every state change, or when read and write workloads have different scaling needs. Event-driven architecture decouples producers from consumers, enabling independent scaling, temporal decoupling, and easier system evolution.
10
+
11
+ ## How It Works
12
+
13
+ ### In-Process Event Bus
14
+
15
+ Start simple with a typed event emitter before reaching for message brokers:
16
+
17
+ ```typescript
18
+ type EventHandler<T = unknown> = (payload: T) => void | Promise<void>;
19
+
20
+ interface AppEvents {
21
+ "order.created": { orderId: string; userId: string; total: number };
22
+ "order.shipped": { orderId: string; trackingNumber: string };
23
+ "user.registered": { userId: string; email: string };
24
+ }
25
+
26
+ class TypedEventBus {
27
+ private handlers = new Map<string, EventHandler[]>();
28
+
29
+ on<K extends keyof AppEvents>(event: K, handler: EventHandler<AppEvents[K]>): void {
30
+ const list = this.handlers.get(event as string) ?? [];
31
+ list.push(handler as EventHandler);
32
+ this.handlers.set(event as string, list);
33
+ }
34
+
35
+ async emit<K extends keyof AppEvents>(event: K, payload: AppEvents[K]): Promise<void> {
36
+ const handlers = this.handlers.get(event as string) ?? [];
37
+ await Promise.allSettled(handlers.map((h) => h(payload)));
38
+ }
39
+ }
40
+
41
+ const bus = new TypedEventBus();
42
+ bus.on("order.created", async (data) => sendConfirmationEmail(data.userId, data.orderId));
43
+ bus.on("order.created", async (data) => updateInventory(data.orderId));
44
+ bus.on("order.created", async (data) => trackAnalytics("order_created", data));
45
+ ```
46
+
47
+ ### Distributed Pub/Sub with Redis Streams
48
+
49
+ ```typescript
50
+ import { Redis } from "ioredis";
51
+
52
+ const redis = new Redis();
53
+
54
+ // Producer
55
+ async function publishEvent(stream: string, event: Record<string, string>) {
56
+ await redis.xadd(stream, "*", ...Object.entries(event).flat());
57
+ }
58
+
59
+ await publishEvent("orders", {
60
+ type: "order.created",
61
+ orderId: "o_123",
62
+ payload: JSON.stringify({ userId: "u_456", total: 99.99 }),
63
+ });
64
+
65
+ // Consumer group -- each service gets every event exactly once
66
+ await redis.xgroup("CREATE", "orders", "email-service", "0", "MKSTREAM").catch(() => {});
67
+
68
+ async function consumeEvents(group: string, consumer: string) {
69
+ while (true) {
70
+ const results = await redis.xreadgroup(
71
+ "GROUP", group, consumer, "COUNT", 10, "BLOCK", 5000, "STREAMS", "orders", ">"
72
+ );
73
+ if (!results) continue;
74
+ for (const [, messages] of results) {
75
+ for (const [id, fields] of messages) {
76
+ await processEvent(Object.fromEntries(chunked(fields, 2)));
77
+ await redis.xack("orders", group, id);
78
+ }
79
+ }
80
+ }
81
+ }
82
+ ```
83
+
84
+ ### CQRS -- Command Query Responsibility Segregation
85
+
86
+ Separate write models from read models:
87
+
88
+ ```typescript
89
+ // Command side -- handles writes, enforces business rules
90
+ class OrderCommandHandler {
91
+ async createOrder(cmd: CreateOrderCommand): Promise<string> {
92
+ const order = Order.create(cmd.userId, cmd.items);
93
+ await this.orderRepo.save(order);
94
+ await this.eventBus.emit("order.created", {
95
+ orderId: order.id,
96
+ userId: cmd.userId,
97
+ total: order.total,
98
+ });
99
+ return order.id;
100
+ }
101
+ }
102
+
103
+ // Query side -- denormalized read model updated by events
104
+ class OrderReadModel {
105
+ async onOrderCreated(event: OrderCreatedEvent) {
106
+ await this.readDb.insert("order_summaries", {
107
+ orderId: event.orderId,
108
+ userId: event.userId,
109
+ total: event.total,
110
+ status: "pending",
111
+ createdAt: event.timestamp,
112
+ });
113
+ }
114
+
115
+ async getOrderSummaries(userId: string) {
116
+ return this.readDb.query(
117
+ "SELECT * FROM order_summaries WHERE user_id = $1 ORDER BY created_at DESC",
118
+ [userId]
119
+ );
120
+ }
121
+ }
122
+ ```
123
+
124
+ ### Event Sourcing
125
+
126
+ Store events as the source of truth, derive state by replaying:
127
+
128
+ ```typescript
129
+ type OrderEvent =
130
+ | { type: "OrderCreated"; orderId: string; items: OrderItem[]; timestamp: string }
131
+ | { type: "PaymentReceived"; orderId: string; amount: number; timestamp: string }
132
+ | { type: "OrderShipped"; orderId: string; trackingId: string; timestamp: string };
133
+
134
+ function buildOrderState(events: OrderEvent[]): OrderState {
135
+ return events.reduce((state, event) => {
136
+ switch (event.type) {
137
+ case "OrderCreated":
138
+ return { ...state, id: event.orderId, items: event.items, status: "pending" };
139
+ case "PaymentReceived":
140
+ return { ...state, status: "paid", paidAmount: event.amount };
141
+ case "OrderShipped":
142
+ return { ...state, status: "shipped", trackingId: event.trackingId };
143
+ }
144
+ }, {} as OrderState);
145
+ }
146
+
147
+ // Append-only event store
148
+ async function appendEvent(aggregateId: string, event: OrderEvent): Promise<void> {
149
+ await db.query(
150
+ "INSERT INTO events (id, aggregate_id, type, data, timestamp) VALUES ($1, $2, $3, $4, $5)",
151
+ [crypto.randomUUID(), aggregateId, event.type, JSON.stringify(event), event.timestamp]
152
+ );
153
+ }
154
+ ```
155
+
156
+ ### Dead Letter Queue
157
+
158
+ Handle failed messages without losing them:
159
+
160
+ ```typescript
161
+ async function processWithDLQ(
162
+ message: QueueMessage,
163
+ handler: (msg: QueueMessage) => Promise<void>,
164
+ maxRetries = 3
165
+ ) {
166
+ const retryCount = message.attributes?.retryCount ?? 0;
167
+ try {
168
+ await handler(message);
169
+ } catch (err) {
170
+ if (retryCount < maxRetries) {
171
+ await queue.send({
172
+ ...message,
173
+ attributes: { retryCount: retryCount + 1 },
174
+ delaySeconds: Math.pow(2, retryCount) * 10,
175
+ });
176
+ } else {
177
+ await dlq.send({
178
+ originalMessage: message,
179
+ error: (err as Error).message,
180
+ failedAt: new Date().toISOString(),
181
+ });
182
+ logger.error({ messageId: message.id }, "Sent to DLQ after max retries");
183
+ }
184
+ }
185
+ }
186
+ ```
187
+
188
+ ### Idempotent Event Handlers
189
+
190
+ Events may be delivered more than once. Make handlers safe to re-run:
191
+
192
+ ```typescript
193
+ async function idempotentHandler(
194
+ eventId: string,
195
+ handler: () => Promise<void>
196
+ ): Promise<void> {
197
+ const acquired = await redis.set(`processed:${eventId}`, "1", "NX", "EX", 86400);
198
+ if (!acquired) {
199
+ logger.info({ eventId }, "Already processed, skipping");
200
+ return;
201
+ }
202
+ await handler();
203
+ }
204
+
205
+ // Or use database constraints
206
+ await db.query(
207
+ `INSERT INTO shipment_notifications (order_id, sent_at)
208
+ VALUES ($1, NOW()) ON CONFLICT (order_id) DO NOTHING`,
209
+ [orderId]
210
+ );
211
+ ```
212
+
213
+ ## Examples
214
+
215
+ | Pattern | When | Result |
216
+ |---------|------|--------|
217
+ | In-process event bus | Monolith | Decoupled handlers, zero latency |
218
+ | Pub/Sub (Redis/RabbitMQ) | Microservices | Cross-service communication |
219
+ | CQRS | Different read/write scaling | Optimized read models |
220
+ | Event sourcing | Audit trail, financial systems | Complete history, replay |
221
+ | Dead letter queue | Failed message handling | No lost messages |
222
+ | Idempotency | At-least-once delivery | Exactly-once processing |
223
+
224
+ ## Checklist
225
+ - [ ] Events named in past tense (`order.created`, not `create.order`)
226
+ - [ ] Every event has ID, timestamp, and aggregate ID
227
+ - [ ] Consumers are idempotent (safe to process same event twice)
228
+ - [ ] Dead letter queues configured for all subscriptions
229
+ - [ ] Event schemas versioned for backward compatibility
230
+ - [ ] CQRS read models documented as eventually consistent
231
+ - [ ] Event store indexed on aggregate_id + version
232
+ - [ ] Failed events trigger alerts, not silent data loss