@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.
- package/assets/agents/accessibility-auditor.md +39 -0
- package/assets/agents/api-designer.md +40 -0
- package/assets/agents/auth-implementer.md +64 -0
- package/assets/agents/bug-hunter.md +42 -0
- package/assets/agents/bundle-analyzer.md +40 -0
- package/assets/agents/cache-optimizer.md +55 -0
- package/assets/agents/changelog-writer.md +55 -0
- package/assets/agents/ci-cd-builder.md +40 -0
- package/assets/agents/code-explainer.md +39 -0
- package/assets/agents/code-reviewer.md +39 -0
- package/assets/agents/cost-optimizer.md +57 -0
- package/assets/agents/cron-scheduler.md +51 -0
- package/assets/agents/data-seeder.md +56 -0
- package/assets/agents/database-architect.md +40 -0
- package/assets/agents/dependency-updater.md +40 -0
- package/assets/agents/deploy-checker.md +40 -0
- package/assets/agents/docker-optimizer.md +40 -0
- package/assets/agents/documentation-writer.md +40 -0
- package/assets/agents/email-builder.md +55 -0
- package/assets/agents/env-setup.md +40 -0
- package/assets/agents/error-handler.md +40 -0
- package/assets/agents/eslint-fixer.md +46 -0
- package/assets/agents/feature-flagger.md +69 -0
- package/assets/agents/git-detective.md +39 -0
- package/assets/agents/graphql-builder.md +60 -0
- package/assets/agents/incident-responder.md +59 -0
- package/assets/agents/log-analyzer.md +39 -0
- package/assets/agents/migration-planner.md +41 -0
- package/assets/agents/monorepo-navigator.md +39 -0
- package/assets/agents/nextjs-expert.md +57 -0
- package/assets/agents/notification-builder.md +56 -0
- package/assets/agents/onboarding-guide.md +39 -0
- package/assets/agents/performance-profiler.md +40 -0
- package/assets/agents/prisma-expert.md +57 -0
- package/assets/agents/rate-limiter.md +58 -0
- package/assets/agents/react-expert.md +58 -0
- package/assets/agents/refactorer.md +42 -0
- package/assets/agents/regex-builder.md +46 -0
- package/assets/agents/release-manager.md +40 -0
- package/assets/agents/s3-manager.md +58 -0
- package/assets/agents/schema-validator.md +40 -0
- package/assets/agents/search-builder.md +62 -0
- package/assets/agents/security-auditor.md +39 -0
- package/assets/agents/sitemap-generator.md +53 -0
- package/assets/agents/stripe-integrator.md +59 -0
- package/assets/agents/tailwind-expert.md +55 -0
- package/assets/agents/tech-debt-tracker.md +39 -0
- package/assets/agents/test-writer.md +42 -0
- package/assets/agents/type-fixer.md +45 -0
- package/assets/agents/webhook-builder.md +54 -0
- package/assets/rules/cpp.md +53 -0
- package/assets/rules/css.md +52 -0
- package/assets/rules/go.md +50 -0
- package/assets/rules/html.md +52 -0
- package/assets/rules/java.md +51 -0
- package/assets/rules/kotlin.md +50 -0
- package/assets/rules/php.md +51 -0
- package/assets/rules/python.md +51 -0
- package/assets/rules/ruby.md +51 -0
- package/assets/rules/rust.md +49 -0
- package/assets/rules/shell.md +52 -0
- package/assets/rules/sql.md +49 -0
- package/assets/rules/swift.md +50 -0
- package/assets/rules/typescript.md +52 -0
- package/assets/rules/yaml-json.md +51 -0
- package/assets/skills/accessibility.md +210 -0
- package/assets/skills/agent-patterns.md +387 -0
- package/assets/skills/ai-integration.md +263 -0
- package/assets/skills/animation-patterns.md +224 -0
- package/assets/skills/api-design.md +218 -0
- package/assets/skills/api-gateway.md +341 -0
- package/assets/skills/api-versioning.md +226 -0
- package/assets/skills/astro-patterns.md +233 -0
- package/assets/skills/auth-patterns.md +248 -0
- package/assets/skills/aws-patterns.md +171 -0
- package/assets/skills/background-jobs.md +162 -0
- package/assets/skills/browser-extensions.md +309 -0
- package/assets/skills/caching-patterns.md +253 -0
- package/assets/skills/ci-cd.md +251 -0
- package/assets/skills/cli-development.md +296 -0
- package/assets/skills/code-review.md +185 -0
- package/assets/skills/cron-patterns.md +327 -0
- package/assets/skills/data-fetching.md +231 -0
- package/assets/skills/database-migrations.md +346 -0
- package/assets/skills/database-patterns.md +219 -0
- package/assets/skills/debugging.md +281 -0
- package/assets/skills/design-system.md +289 -0
- package/assets/skills/django-patterns.md +182 -0
- package/assets/skills/docker-patterns.md +235 -0
- package/assets/skills/e2e-testing.md +287 -0
- package/assets/skills/edge-computing.md +268 -0
- package/assets/skills/electron-patterns.md +266 -0
- package/assets/skills/email-templates.md +206 -0
- package/assets/skills/error-handling.md +265 -0
- package/assets/skills/event-driven.md +232 -0
- package/assets/skills/express-patterns.md +239 -0
- package/assets/skills/fastapi-patterns.md +198 -0
- package/assets/skills/feature-flags.md +212 -0
- package/assets/skills/figma-to-code.md +298 -0
- package/assets/skills/file-upload.md +228 -0
- package/assets/skills/forms-patterns.md +264 -0
- package/assets/skills/gcp-patterns.md +189 -0
- package/assets/skills/git-workflow.md +187 -0
- package/assets/skills/golang-patterns.md +185 -0
- package/assets/skills/graphql-patterns.md +244 -0
- package/assets/skills/i18n-patterns.md +172 -0
- package/assets/skills/image-processing.md +350 -0
- package/assets/skills/java-springboot.md +226 -0
- package/assets/skills/kotlin-patterns.md +207 -0
- package/assets/skills/kubernetes-patterns.md +326 -0
- package/assets/skills/laravel-patterns.md +261 -0
- package/assets/skills/llm-fine-tuning.md +335 -0
- package/assets/skills/load-testing.md +303 -0
- package/assets/skills/logging-observability.md +228 -0
- package/assets/skills/markdown-processing.md +318 -0
- package/assets/skills/mcp-server-patterns.md +292 -0
- package/assets/skills/microservices.md +272 -0
- package/assets/skills/migration-patterns.md +239 -0
- package/assets/skills/mongodb-patterns.md +189 -0
- package/assets/skills/monorepo-patterns.md +287 -0
- package/assets/skills/nextjs-app-router.md +237 -0
- package/assets/skills/notification-patterns.md +348 -0
- package/assets/skills/oauth-patterns.md +246 -0
- package/assets/skills/payment-integration.md +222 -0
- package/assets/skills/pdf-generation.md +307 -0
- package/assets/skills/performance-optimization.md +277 -0
- package/assets/skills/php-patterns.md +210 -0
- package/assets/skills/prisma-patterns.md +241 -0
- package/assets/skills/prompt-engineering.md +193 -0
- package/assets/skills/pwa-patterns.md +247 -0
- package/assets/skills/python-patterns.md +158 -0
- package/assets/skills/python-testing.md +172 -0
- package/assets/skills/queue-patterns.md +295 -0
- package/assets/skills/rag-patterns.md +159 -0
- package/assets/skills/rate-limiting.md +319 -0
- package/assets/skills/react-components.md +201 -0
- package/assets/skills/react-native-patterns.md +299 -0
- package/assets/skills/real-time-patterns.md +181 -0
- package/assets/skills/redis-patterns.md +188 -0
- package/assets/skills/refactoring.md +218 -0
- package/assets/skills/regex-patterns.md +191 -0
- package/assets/skills/remix-patterns.md +262 -0
- package/assets/skills/responsive-design.md +199 -0
- package/assets/skills/ruby-rails-patterns.md +178 -0
- package/assets/skills/rust-patterns.md +211 -0
- package/assets/skills/search-patterns.md +227 -0
- package/assets/skills/security-hardening.md +237 -0
- package/assets/skills/seo-patterns.md +179 -0
- package/assets/skills/serverless-patterns.md +223 -0
- package/assets/skills/sql-optimization.md +154 -0
- package/assets/skills/state-management.md +254 -0
- package/assets/skills/storybook-patterns.md +330 -0
- package/assets/skills/svelte-patterns.md +258 -0
- package/assets/skills/swift-patterns.md +227 -0
- package/assets/skills/tailwind-patterns.md +272 -0
- package/assets/skills/tdd-workflow.md +199 -0
- package/assets/skills/terraform-patterns.md +270 -0
- package/assets/skills/testing-react.md +240 -0
- package/assets/skills/testing-vitest.md +232 -0
- package/assets/skills/typescript-strict.md +159 -0
- package/assets/skills/video-processing.md +340 -0
- package/assets/skills/vue-patterns.md +247 -0
- package/assets/skills/web-workers.md +327 -0
- package/assets/skills/webhooks-patterns.md +283 -0
- package/assets/skills/websocket-patterns.md +306 -0
- package/dist/cli/index.js +941 -958
- package/dist/core/index.d.ts +341 -11
- package/dist/core.js +58 -95
- package/dist/mcp/index.d.ts +33 -1
- package/dist/mcp-server.js +177 -255
- package/package.json +4 -1
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: api-gateway
|
|
3
|
+
description: API gateway patterns for routing, rate limiting, authentication, request transformation, load balancing, and health checks.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# API Gateway Patterns
|
|
7
|
+
|
|
8
|
+
## When to Use
|
|
9
|
+
Implement an API gateway when you have multiple backend services and need a unified entry point for clients. Gateways handle cross-cutting concerns: authentication, rate limiting, request routing, response transformation, and load balancing. Use these patterns in microservice architectures, BFF (Backend for Frontend) setups, or any system where you need to decouple client-facing APIs from internal service topology.
|
|
10
|
+
|
|
11
|
+
## How It Works
|
|
12
|
+
|
|
13
|
+
### Express-Based API Gateway
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
// src/gateway.ts
|
|
17
|
+
import express from 'express';
|
|
18
|
+
import { createProxyMiddleware } from 'http-proxy-middleware';
|
|
19
|
+
import rateLimit from 'express-rate-limit';
|
|
20
|
+
import helmet from 'helmet';
|
|
21
|
+
import cors from 'cors';
|
|
22
|
+
|
|
23
|
+
const app = express();
|
|
24
|
+
|
|
25
|
+
// Security headers
|
|
26
|
+
app.use(helmet());
|
|
27
|
+
app.use(cors({ origin: process.env.ALLOWED_ORIGINS?.split(','), credentials: true }));
|
|
28
|
+
app.use(express.json({ limit: '1mb' }));
|
|
29
|
+
|
|
30
|
+
// Service registry
|
|
31
|
+
const services: Record<string, { target: string; healthPath: string }> = {
|
|
32
|
+
users: { target: 'http://user-service:3001', healthPath: '/health' },
|
|
33
|
+
posts: { target: 'http://post-service:3002', healthPath: '/health' },
|
|
34
|
+
billing: { target: 'http://billing-service:3003', healthPath: '/health' },
|
|
35
|
+
search: { target: 'http://search-service:3004', healthPath: '/health' },
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// Global rate limiter
|
|
39
|
+
app.use(rateLimit({
|
|
40
|
+
windowMs: 60_000,
|
|
41
|
+
max: 100,
|
|
42
|
+
standardHeaders: true,
|
|
43
|
+
legacyHeaders: false,
|
|
44
|
+
keyGenerator: (req) => req.headers['x-forwarded-for'] as string || req.ip,
|
|
45
|
+
message: { error: 'Too many requests', retryAfter: 60 },
|
|
46
|
+
}));
|
|
47
|
+
|
|
48
|
+
// Route to services
|
|
49
|
+
for (const [name, config] of Object.entries(services)) {
|
|
50
|
+
app.use(`/api/${name}`, createProxyMiddleware({
|
|
51
|
+
target: config.target,
|
|
52
|
+
changeOrigin: true,
|
|
53
|
+
pathRewrite: { [`^/api/${name}`]: '' },
|
|
54
|
+
timeout: 30_000,
|
|
55
|
+
proxyTimeout: 30_000,
|
|
56
|
+
on: {
|
|
57
|
+
proxyReq: (proxyReq, req) => {
|
|
58
|
+
// Forward auth context
|
|
59
|
+
if ((req as any).userId) {
|
|
60
|
+
proxyReq.setHeader('X-User-Id', (req as any).userId);
|
|
61
|
+
}
|
|
62
|
+
proxyReq.setHeader('X-Request-Id', req.headers['x-request-id'] || crypto.randomUUID());
|
|
63
|
+
},
|
|
64
|
+
error: (err, req, res) => {
|
|
65
|
+
console.error(`Proxy error [${name}]:`, err.message);
|
|
66
|
+
(res as express.Response).status(502).json({
|
|
67
|
+
error: 'Service unavailable',
|
|
68
|
+
service: name,
|
|
69
|
+
});
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
}));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
app.listen(3000, () => console.log('Gateway listening on :3000'));
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Authentication Middleware
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
// src/middleware/auth.ts
|
|
82
|
+
import jwt from 'jsonwebtoken';
|
|
83
|
+
import type { Request, Response, NextFunction } from 'express';
|
|
84
|
+
|
|
85
|
+
interface AuthConfig {
|
|
86
|
+
publicPaths: string[];
|
|
87
|
+
jwtSecret: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function authMiddleware(config: AuthConfig) {
|
|
91
|
+
return (req: Request, res: Response, next: NextFunction) => {
|
|
92
|
+
// Skip auth for public paths
|
|
93
|
+
if (config.publicPaths.some((p) => req.path.startsWith(p))) {
|
|
94
|
+
return next();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const authHeader = req.headers.authorization;
|
|
98
|
+
if (!authHeader?.startsWith('Bearer ')) {
|
|
99
|
+
return res.status(401).json({ error: 'Missing authorization header' });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const token = authHeader.slice(7);
|
|
103
|
+
try {
|
|
104
|
+
const payload = jwt.verify(token, config.jwtSecret) as { userId: string; role: string };
|
|
105
|
+
(req as any).userId = payload.userId;
|
|
106
|
+
(req as any).userRole = payload.role;
|
|
107
|
+
next();
|
|
108
|
+
} catch (err) {
|
|
109
|
+
if ((err as jwt.JsonWebTokenError).name === 'TokenExpiredError') {
|
|
110
|
+
return res.status(401).json({ error: 'Token expired', code: 'TOKEN_EXPIRED' });
|
|
111
|
+
}
|
|
112
|
+
return res.status(401).json({ error: 'Invalid token' });
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Register before proxy routes
|
|
118
|
+
app.use(authMiddleware({
|
|
119
|
+
publicPaths: ['/api/auth', '/health', '/api/docs'],
|
|
120
|
+
jwtSecret: process.env.JWT_SECRET!,
|
|
121
|
+
}));
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Per-Route Rate Limiting
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
// src/middleware/rate-limit.ts
|
|
128
|
+
import rateLimit from 'express-rate-limit';
|
|
129
|
+
import RedisStore from 'rate-limit-redis';
|
|
130
|
+
import { createClient } from 'redis';
|
|
131
|
+
|
|
132
|
+
const redis = createClient({ url: process.env.REDIS_URL });
|
|
133
|
+
await redis.connect();
|
|
134
|
+
|
|
135
|
+
export function createRateLimiter(windowMs: number, max: number) {
|
|
136
|
+
return rateLimit({
|
|
137
|
+
windowMs,
|
|
138
|
+
max,
|
|
139
|
+
standardHeaders: true,
|
|
140
|
+
store: new RedisStore({
|
|
141
|
+
sendCommand: (...args: string[]) => redis.sendCommand(args),
|
|
142
|
+
}),
|
|
143
|
+
keyGenerator: (req) => {
|
|
144
|
+
const userId = (req as any).userId;
|
|
145
|
+
return userId ? `user:${userId}` : req.ip;
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Different limits per route
|
|
151
|
+
app.use('/api/auth/login', createRateLimiter(15 * 60_000, 5)); // 5 per 15 min
|
|
152
|
+
app.use('/api/search', createRateLimiter(60_000, 30)); // 30 per min
|
|
153
|
+
app.use('/api/billing', createRateLimiter(60_000, 10)); // 10 per min
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Request/Response Transformation
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
// src/middleware/transform.ts
|
|
160
|
+
import type { Request, Response, NextFunction } from 'express';
|
|
161
|
+
|
|
162
|
+
// Version-based response transformation
|
|
163
|
+
export function apiVersionTransform(req: Request, res: Response, next: NextFunction) {
|
|
164
|
+
const version = req.headers['api-version'] || 'v2';
|
|
165
|
+
|
|
166
|
+
// Intercept response for transformation
|
|
167
|
+
const originalJson = res.json.bind(res);
|
|
168
|
+
res.json = function (body: any) {
|
|
169
|
+
if (version === 'v1' && body?.data) {
|
|
170
|
+
// v1 clients expect flat response, v2 uses envelope
|
|
171
|
+
return originalJson(body.data);
|
|
172
|
+
}
|
|
173
|
+
return originalJson(body);
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
next();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Request enrichment
|
|
180
|
+
export function enrichRequest(req: Request, _res: Response, next: NextFunction) {
|
|
181
|
+
const requestId = req.headers['x-request-id'] || crypto.randomUUID();
|
|
182
|
+
req.headers['x-request-id'] = requestId;
|
|
183
|
+
req.headers['x-gateway-timestamp'] = new Date().toISOString();
|
|
184
|
+
req.headers['x-client-ip'] = req.headers['x-forwarded-for'] as string || req.ip;
|
|
185
|
+
next();
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Health Check Aggregation
|
|
190
|
+
|
|
191
|
+
```typescript
|
|
192
|
+
// src/health.ts
|
|
193
|
+
import type { Request, Response } from 'express';
|
|
194
|
+
|
|
195
|
+
interface ServiceHealth {
|
|
196
|
+
name: string;
|
|
197
|
+
status: 'healthy' | 'degraded' | 'down';
|
|
198
|
+
latencyMs: number;
|
|
199
|
+
lastChecked: string;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function checkServiceHealth(name: string, url: string): Promise<ServiceHealth> {
|
|
203
|
+
const start = Date.now();
|
|
204
|
+
try {
|
|
205
|
+
const controller = new AbortController();
|
|
206
|
+
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
207
|
+
const res = await fetch(url, { signal: controller.signal });
|
|
208
|
+
clearTimeout(timeout);
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
name,
|
|
212
|
+
status: res.ok ? 'healthy' : 'degraded',
|
|
213
|
+
latencyMs: Date.now() - start,
|
|
214
|
+
lastChecked: new Date().toISOString(),
|
|
215
|
+
};
|
|
216
|
+
} catch {
|
|
217
|
+
return {
|
|
218
|
+
name,
|
|
219
|
+
status: 'down',
|
|
220
|
+
latencyMs: Date.now() - start,
|
|
221
|
+
lastChecked: new Date().toISOString(),
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
app.get('/health', async (_req: Request, res: Response) => {
|
|
227
|
+
const checks = await Promise.all(
|
|
228
|
+
Object.entries(services).map(([name, config]) =>
|
|
229
|
+
checkServiceHealth(name, `${config.target}${config.healthPath}`)
|
|
230
|
+
)
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
const allHealthy = checks.every((c) => c.status === 'healthy');
|
|
234
|
+
const anyDown = checks.some((c) => c.status === 'down');
|
|
235
|
+
|
|
236
|
+
res.status(anyDown ? 503 : 200).json({
|
|
237
|
+
status: anyDown ? 'degraded' : allHealthy ? 'healthy' : 'degraded',
|
|
238
|
+
services: checks,
|
|
239
|
+
uptime: process.uptime(),
|
|
240
|
+
timestamp: new Date().toISOString(),
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// Liveness probe (lightweight)
|
|
245
|
+
app.get('/health/live', (_req, res) => res.status(200).send('ok'));
|
|
246
|
+
|
|
247
|
+
// Readiness probe (checks dependencies)
|
|
248
|
+
app.get('/health/ready', async (_req, res) => {
|
|
249
|
+
const critical = ['users', 'billing'];
|
|
250
|
+
const checks = await Promise.all(
|
|
251
|
+
critical.map((name) =>
|
|
252
|
+
checkServiceHealth(name, `${services[name].target}${services[name].healthPath}`)
|
|
253
|
+
)
|
|
254
|
+
);
|
|
255
|
+
const ready = checks.every((c) => c.status !== 'down');
|
|
256
|
+
res.status(ready ? 200 : 503).json({ ready, services: checks });
|
|
257
|
+
});
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### Circuit Breaker
|
|
261
|
+
|
|
262
|
+
```typescript
|
|
263
|
+
// src/circuit-breaker.ts
|
|
264
|
+
type State = 'closed' | 'open' | 'half-open';
|
|
265
|
+
|
|
266
|
+
export class CircuitBreaker {
|
|
267
|
+
private state: State = 'closed';
|
|
268
|
+
private failures = 0;
|
|
269
|
+
private lastFailure = 0;
|
|
270
|
+
private successCount = 0;
|
|
271
|
+
|
|
272
|
+
constructor(
|
|
273
|
+
private readonly threshold: number = 5,
|
|
274
|
+
private readonly resetTimeoutMs: number = 30_000,
|
|
275
|
+
private readonly halfOpenMax: number = 3,
|
|
276
|
+
) {}
|
|
277
|
+
|
|
278
|
+
async execute<T>(fn: () => Promise<T>): Promise<T> {
|
|
279
|
+
if (this.state === 'open') {
|
|
280
|
+
if (Date.now() - this.lastFailure > this.resetTimeoutMs) {
|
|
281
|
+
this.state = 'half-open';
|
|
282
|
+
this.successCount = 0;
|
|
283
|
+
} else {
|
|
284
|
+
throw new Error('Circuit breaker is open');
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
try {
|
|
289
|
+
const result = await fn();
|
|
290
|
+
this.onSuccess();
|
|
291
|
+
return result;
|
|
292
|
+
} catch (err) {
|
|
293
|
+
this.onFailure();
|
|
294
|
+
throw err;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
private onSuccess() {
|
|
299
|
+
if (this.state === 'half-open') {
|
|
300
|
+
this.successCount++;
|
|
301
|
+
if (this.successCount >= this.halfOpenMax) {
|
|
302
|
+
this.state = 'closed';
|
|
303
|
+
this.failures = 0;
|
|
304
|
+
}
|
|
305
|
+
} else {
|
|
306
|
+
this.failures = 0;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
private onFailure() {
|
|
311
|
+
this.failures++;
|
|
312
|
+
this.lastFailure = Date.now();
|
|
313
|
+
if (this.failures >= this.threshold) {
|
|
314
|
+
this.state = 'open';
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
getState(): State { return this.state; }
|
|
319
|
+
}
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
## Examples
|
|
323
|
+
|
|
324
|
+
| Concern | Implementation | Where |
|
|
325
|
+
|---------|---------------|-------|
|
|
326
|
+
| Auth | JWT verification middleware | Before proxy |
|
|
327
|
+
| Rate limiting | Redis-backed sliding window | Per-route |
|
|
328
|
+
| Routing | `http-proxy-middleware` | Path prefix mapping |
|
|
329
|
+
| Transformation | Response interceptor | API version compat |
|
|
330
|
+
| Health | Aggregated service checks | `/health` endpoint |
|
|
331
|
+
| Circuit breaking | Failure counter + timeout | Per-service proxy |
|
|
332
|
+
|
|
333
|
+
## Checklist
|
|
334
|
+
- [ ] All requests authenticated before reaching backend services
|
|
335
|
+
- [ ] Rate limiting applied globally and per-route with Redis backing
|
|
336
|
+
- [ ] Proxy forwards `X-Request-Id` for distributed tracing
|
|
337
|
+
- [ ] Service errors return 502 with service name, not raw proxy errors
|
|
338
|
+
- [ ] Health endpoint checks all downstream services with timeout
|
|
339
|
+
- [ ] Liveness and readiness probes separated for Kubernetes
|
|
340
|
+
- [ ] Circuit breaker prevents cascading failures between services
|
|
341
|
+
- [ ] Request/response logged with request ID for debugging
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: api-versioning
|
|
3
|
+
description: API versioning patterns with URL, header, and query param versioning, deprecation strategy, and changelog.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# API Versioning
|
|
7
|
+
|
|
8
|
+
## When to Use
|
|
9
|
+
Apply when your API has external consumers and you need to make breaking changes without disrupting them. Version your API from day one. Use URL-based versioning for simplicity, header-based for cleanliness, and maintain a deprecation strategy that gives consumers time to migrate.
|
|
10
|
+
|
|
11
|
+
## How It Works
|
|
12
|
+
|
|
13
|
+
### URL-Based Versioning (Recommended Default)
|
|
14
|
+
|
|
15
|
+
The most common and discoverable approach:
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import { Router } from "express";
|
|
19
|
+
|
|
20
|
+
const v1Router = Router();
|
|
21
|
+
const v2Router = Router();
|
|
22
|
+
|
|
23
|
+
v1Router.get("/users/:id", async (req, res) => {
|
|
24
|
+
const user = await userService.getById(req.params.id);
|
|
25
|
+
res.json({ id: user.id, name: user.fullName, email: user.email });
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
v2Router.get("/users/:id", async (req, res) => {
|
|
29
|
+
const user = await userService.getById(req.params.id);
|
|
30
|
+
res.json({
|
|
31
|
+
id: user.id,
|
|
32
|
+
name: { first: user.firstName, last: user.lastName },
|
|
33
|
+
email: user.email,
|
|
34
|
+
createdAt: user.createdAt.toISOString(),
|
|
35
|
+
links: { self: `/api/v2/users/${user.id}` },
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
app.use("/api/v1", v1Router);
|
|
40
|
+
app.use("/api/v2", v2Router);
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Header-Based Versioning
|
|
44
|
+
|
|
45
|
+
Keeps URLs clean; version is in the Accept header:
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
function versionMiddleware(req: Request, _res: Response, next: NextFunction) {
|
|
49
|
+
const accept = req.headers.accept ?? "";
|
|
50
|
+
const match = accept.match(/application\/vnd\.myapp\.v(\d+)\+json/);
|
|
51
|
+
req.apiVersion = match ? parseInt(match[1]) : 1;
|
|
52
|
+
next();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
app.use(versionMiddleware);
|
|
56
|
+
|
|
57
|
+
app.get("/api/users/:id", (req, res) => {
|
|
58
|
+
switch (req.apiVersion) {
|
|
59
|
+
case 2: return getUserV2(req, res);
|
|
60
|
+
default: return getUserV1(req, res);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Client sends: `Accept: application/vnd.myapp.v2+json`
|
|
66
|
+
|
|
67
|
+
### Deprecation Strategy
|
|
68
|
+
|
|
69
|
+
Give consumers clear warnings and a migration timeline:
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
function deprecationMiddleware(
|
|
73
|
+
deprecatedVersion: number,
|
|
74
|
+
sunsetDate: string,
|
|
75
|
+
migrationGuide: string
|
|
76
|
+
) {
|
|
77
|
+
return (req: Request, res: Response, next: NextFunction) => {
|
|
78
|
+
if (req.apiVersion <= deprecatedVersion) {
|
|
79
|
+
res.setHeader("Deprecation", "true");
|
|
80
|
+
res.setHeader("Sunset", new Date(sunsetDate).toUTCString());
|
|
81
|
+
res.setHeader("Link", `<${migrationGuide}>; rel="deprecation"`);
|
|
82
|
+
logger.warn({
|
|
83
|
+
apiVersion: req.apiVersion,
|
|
84
|
+
path: req.path,
|
|
85
|
+
clientId: req.headers["x-client-id"],
|
|
86
|
+
}, "Deprecated API version called");
|
|
87
|
+
}
|
|
88
|
+
next();
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
app.use(deprecationMiddleware(
|
|
93
|
+
1,
|
|
94
|
+
"2027-06-01",
|
|
95
|
+
"https://docs.example.com/api/migration-v2"
|
|
96
|
+
));
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Breaking vs. Non-Breaking Changes
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
NON-BREAKING (no new version needed):
|
|
103
|
+
- Adding a new field to the response
|
|
104
|
+
- Adding a new optional query parameter
|
|
105
|
+
- Adding a new endpoint
|
|
106
|
+
- Making a required field optional
|
|
107
|
+
|
|
108
|
+
BREAKING (needs new version):
|
|
109
|
+
- Removing or renaming a field
|
|
110
|
+
- Changing a field's type (string -> number)
|
|
111
|
+
- Changing the response structure (flat -> nested)
|
|
112
|
+
- Making an optional field required
|
|
113
|
+
- Changing error response format
|
|
114
|
+
- Changing authentication method
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Version Negotiation Middleware
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
interface VersionConfig {
|
|
121
|
+
supported: number[];
|
|
122
|
+
current: number;
|
|
123
|
+
deprecated: number[];
|
|
124
|
+
sunset: Record<number, string>;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const versionConfig: VersionConfig = {
|
|
128
|
+
supported: [1, 2, 3],
|
|
129
|
+
current: 3,
|
|
130
|
+
deprecated: [1],
|
|
131
|
+
sunset: { 1: "2027-03-01T00:00:00Z" },
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
function versionNegotiation(config: VersionConfig) {
|
|
135
|
+
return (req: Request, res: Response, next: NextFunction) => {
|
|
136
|
+
const requested = parseInt(req.headers["x-api-version"] as string) || config.current;
|
|
137
|
+
|
|
138
|
+
if (!config.supported.includes(requested)) {
|
|
139
|
+
return res.status(400).json({
|
|
140
|
+
error: `API version ${requested} not supported`,
|
|
141
|
+
supported: config.supported,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (config.deprecated.includes(requested)) {
|
|
146
|
+
res.setHeader("Deprecation", "true");
|
|
147
|
+
if (config.sunset[requested]) {
|
|
148
|
+
res.setHeader("Sunset", config.sunset[requested]);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
req.apiVersion = requested;
|
|
153
|
+
res.setHeader("X-API-Version", String(requested));
|
|
154
|
+
next();
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### API Changelog
|
|
160
|
+
|
|
161
|
+
```markdown
|
|
162
|
+
## v2.3.0 -- 2026-06-15
|
|
163
|
+
### Added
|
|
164
|
+
- `GET /users/:id` now includes `createdAt` field
|
|
165
|
+
- New endpoint: `POST /users/:id/avatar`
|
|
166
|
+
### Deprecated
|
|
167
|
+
- `items` field in `POST /orders` -- use `lineItems`. Sunset: 2027-01-01.
|
|
168
|
+
- `GET /api/v1/*` -- migrate to v2. Sunset: 2027-03-01.
|
|
169
|
+
|
|
170
|
+
## v2.2.0 -- 2026-04-20
|
|
171
|
+
### Fixed
|
|
172
|
+
- Pagination correctly returns `hasNextPage: false` on last page
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### OpenAPI Spec Per Version
|
|
176
|
+
|
|
177
|
+
```yaml
|
|
178
|
+
openapi: 3.0.3
|
|
179
|
+
info:
|
|
180
|
+
title: My API
|
|
181
|
+
version: "2.0.0"
|
|
182
|
+
servers:
|
|
183
|
+
- url: https://api.example.com/api/v2
|
|
184
|
+
paths:
|
|
185
|
+
/users:
|
|
186
|
+
get:
|
|
187
|
+
summary: List users
|
|
188
|
+
parameters:
|
|
189
|
+
- name: cursor
|
|
190
|
+
in: query
|
|
191
|
+
schema: { type: string }
|
|
192
|
+
- name: limit
|
|
193
|
+
in: query
|
|
194
|
+
schema: { type: integer, default: 20, maximum: 100 }
|
|
195
|
+
responses:
|
|
196
|
+
"200":
|
|
197
|
+
content:
|
|
198
|
+
application/json:
|
|
199
|
+
schema:
|
|
200
|
+
type: object
|
|
201
|
+
properties:
|
|
202
|
+
data:
|
|
203
|
+
type: array
|
|
204
|
+
items: { $ref: "#/components/schemas/UserV2" }
|
|
205
|
+
pagination:
|
|
206
|
+
$ref: "#/components/schemas/CursorPagination"
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## Examples
|
|
210
|
+
|
|
211
|
+
| Strategy | Pros | Cons |
|
|
212
|
+
|----------|------|------|
|
|
213
|
+
| URL (`/api/v2/`) | Discoverable, cacheable | URL pollution |
|
|
214
|
+
| Header (`Accept: vnd.app.v2`) | Clean URLs | Less discoverable |
|
|
215
|
+
| Query param (`?version=2`) | Easy to test | Pollutes caching |
|
|
216
|
+
| GraphQL `@deprecated` | No versioning needed | Requires schema discipline |
|
|
217
|
+
|
|
218
|
+
## Checklist
|
|
219
|
+
- [ ] API versioned from v1 on day one
|
|
220
|
+
- [ ] Versioning strategy is consistent (URL or header -- pick one)
|
|
221
|
+
- [ ] Breaking changes only in new version numbers
|
|
222
|
+
- [ ] Deprecated versions send `Deprecation` and `Sunset` headers
|
|
223
|
+
- [ ] Migration guide published for every breaking change
|
|
224
|
+
- [ ] Changelog maintained with every release
|
|
225
|
+
- [ ] OpenAPI spec exists per active version
|
|
226
|
+
- [ ] Old versions have sunset date and caller monitoring
|