omgkit 2.0.6 → 2.1.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/package.json +6 -3
- package/plugin/agents/architect.md +357 -43
- package/plugin/agents/code-reviewer.md +481 -22
- package/plugin/agents/debugger.md +397 -30
- package/plugin/agents/docs-manager.md +431 -23
- package/plugin/agents/fullstack-developer.md +395 -34
- package/plugin/agents/git-manager.md +438 -20
- package/plugin/agents/oracle.md +329 -53
- package/plugin/agents/planner.md +275 -32
- package/plugin/agents/researcher.md +343 -21
- package/plugin/agents/scout.md +423 -18
- package/plugin/agents/sprint-master.md +418 -48
- package/plugin/agents/tester.md +551 -26
- package/plugin/skills/backend/api-architecture/SKILL.md +857 -0
- package/plugin/skills/backend/caching-strategies/SKILL.md +755 -0
- package/plugin/skills/backend/event-driven-architecture/SKILL.md +753 -0
- package/plugin/skills/backend/real-time-systems/SKILL.md +635 -0
- package/plugin/skills/databases/database-optimization/SKILL.md +571 -0
- package/plugin/skills/devops/monorepo-management/SKILL.md +595 -0
- package/plugin/skills/devops/observability/SKILL.md +622 -0
- package/plugin/skills/devops/performance-profiling/SKILL.md +905 -0
- package/plugin/skills/frontend/advanced-ui-design/SKILL.md +426 -0
- package/plugin/skills/integrations/ai-integration/SKILL.md +730 -0
- package/plugin/skills/integrations/payment-integration/SKILL.md +735 -0
- package/plugin/skills/methodology/problem-solving/SKILL.md +355 -0
- package/plugin/skills/methodology/research-validation/SKILL.md +668 -0
- package/plugin/skills/methodology/sequential-thinking/SKILL.md +260 -0
- package/plugin/skills/mobile/mobile-development/SKILL.md +756 -0
- package/plugin/skills/security/security-hardening/SKILL.md +633 -0
- package/plugin/skills/tools/document-processing/SKILL.md +916 -0
- package/plugin/skills/tools/image-processing/SKILL.md +748 -0
- package/plugin/skills/tools/mcp-development/SKILL.md +883 -0
- package/plugin/skills/tools/media-processing/SKILL.md +831 -0
|
@@ -0,0 +1,857 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: api-architecture
|
|
3
|
+
description: Enterprise API design with REST, GraphQL, gRPC patterns including versioning, pagination, and error handling
|
|
4
|
+
category: backend
|
|
5
|
+
triggers:
|
|
6
|
+
- api architecture
|
|
7
|
+
- api design
|
|
8
|
+
- rest api
|
|
9
|
+
- graphql
|
|
10
|
+
- grpc
|
|
11
|
+
- api versioning
|
|
12
|
+
- pagination
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
# API Architecture
|
|
16
|
+
|
|
17
|
+
Enterprise-grade **API design patterns** following BigTech standards. This skill covers REST, GraphQL, and gRPC design with versioning, pagination, rate limiting, and comprehensive error handling.
|
|
18
|
+
|
|
19
|
+
## Purpose
|
|
20
|
+
|
|
21
|
+
Design APIs that scale and delight developers:
|
|
22
|
+
|
|
23
|
+
- Apply REST best practices consistently
|
|
24
|
+
- Implement GraphQL for flexible queries
|
|
25
|
+
- Design gRPC for high-performance services
|
|
26
|
+
- Handle versioning without breaking clients
|
|
27
|
+
- Implement robust pagination patterns
|
|
28
|
+
- Create comprehensive error responses
|
|
29
|
+
|
|
30
|
+
## Features
|
|
31
|
+
|
|
32
|
+
### 1. RESTful API Design
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
// Express router with best practices
|
|
36
|
+
import express from 'express';
|
|
37
|
+
import { z } from 'zod';
|
|
38
|
+
|
|
39
|
+
const router = express.Router();
|
|
40
|
+
|
|
41
|
+
// Resource naming conventions
|
|
42
|
+
// ✓ /users (collection)
|
|
43
|
+
// ✓ /users/:id (resource)
|
|
44
|
+
// ✓ /users/:id/posts (sub-collection)
|
|
45
|
+
// ✗ /getUsers, /createUser (verbs in URL)
|
|
46
|
+
|
|
47
|
+
// GET /api/v1/users - List users with pagination
|
|
48
|
+
const ListUsersSchema = z.object({
|
|
49
|
+
page: z.coerce.number().min(1).default(1),
|
|
50
|
+
limit: z.coerce.number().min(1).max(100).default(20),
|
|
51
|
+
sort: z.enum(['created_at', 'name', 'email']).default('created_at'),
|
|
52
|
+
order: z.enum(['asc', 'desc']).default('desc'),
|
|
53
|
+
status: z.enum(['active', 'inactive', 'all']).optional(),
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
router.get('/users', async (req, res) => {
|
|
57
|
+
const query = ListUsersSchema.parse(req.query);
|
|
58
|
+
|
|
59
|
+
const { users, total } = await userService.list(query);
|
|
60
|
+
|
|
61
|
+
// Consistent response envelope
|
|
62
|
+
res.json({
|
|
63
|
+
data: users,
|
|
64
|
+
pagination: {
|
|
65
|
+
page: query.page,
|
|
66
|
+
limit: query.limit,
|
|
67
|
+
total,
|
|
68
|
+
totalPages: Math.ceil(total / query.limit),
|
|
69
|
+
hasMore: query.page * query.limit < total,
|
|
70
|
+
},
|
|
71
|
+
links: {
|
|
72
|
+
self: `/api/v1/users?page=${query.page}&limit=${query.limit}`,
|
|
73
|
+
first: `/api/v1/users?page=1&limit=${query.limit}`,
|
|
74
|
+
last: `/api/v1/users?page=${Math.ceil(total / query.limit)}&limit=${query.limit}`,
|
|
75
|
+
next: query.page * query.limit < total
|
|
76
|
+
? `/api/v1/users?page=${query.page + 1}&limit=${query.limit}`
|
|
77
|
+
: null,
|
|
78
|
+
prev: query.page > 1
|
|
79
|
+
? `/api/v1/users?page=${query.page - 1}&limit=${query.limit}`
|
|
80
|
+
: null,
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// GET /api/v1/users/:id - Get single user
|
|
86
|
+
router.get('/users/:id', async (req, res) => {
|
|
87
|
+
const user = await userService.findById(req.params.id);
|
|
88
|
+
|
|
89
|
+
if (!user) {
|
|
90
|
+
return res.status(404).json({
|
|
91
|
+
error: {
|
|
92
|
+
code: 'USER_NOT_FOUND',
|
|
93
|
+
message: 'User not found',
|
|
94
|
+
details: { id: req.params.id },
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
res.json({ data: user });
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// POST /api/v1/users - Create user
|
|
103
|
+
const CreateUserSchema = z.object({
|
|
104
|
+
email: z.string().email(),
|
|
105
|
+
name: z.string().min(2).max(100),
|
|
106
|
+
password: z.string().min(8),
|
|
107
|
+
role: z.enum(['user', 'admin']).default('user'),
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
router.post('/users', async (req, res) => {
|
|
111
|
+
const data = CreateUserSchema.parse(req.body);
|
|
112
|
+
|
|
113
|
+
const user = await userService.create(data);
|
|
114
|
+
|
|
115
|
+
// Return 201 with Location header
|
|
116
|
+
res.status(201)
|
|
117
|
+
.location(`/api/v1/users/${user.id}`)
|
|
118
|
+
.json({ data: user });
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// PATCH /api/v1/users/:id - Partial update
|
|
122
|
+
const UpdateUserSchema = CreateUserSchema.partial().omit({ password: true });
|
|
123
|
+
|
|
124
|
+
router.patch('/users/:id', async (req, res) => {
|
|
125
|
+
const data = UpdateUserSchema.parse(req.body);
|
|
126
|
+
|
|
127
|
+
const user = await userService.update(req.params.id, data);
|
|
128
|
+
|
|
129
|
+
if (!user) {
|
|
130
|
+
return res.status(404).json({
|
|
131
|
+
error: { code: 'USER_NOT_FOUND', message: 'User not found' },
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
res.json({ data: user });
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// DELETE /api/v1/users/:id - Delete user
|
|
139
|
+
router.delete('/users/:id', async (req, res) => {
|
|
140
|
+
const deleted = await userService.delete(req.params.id);
|
|
141
|
+
|
|
142
|
+
if (!deleted) {
|
|
143
|
+
return res.status(404).json({
|
|
144
|
+
error: { code: 'USER_NOT_FOUND', message: 'User not found' },
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
res.status(204).send();
|
|
149
|
+
});
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### 2. Error Handling Standards
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
// Standard error response format
|
|
156
|
+
interface APIError {
|
|
157
|
+
code: string; // Machine-readable error code
|
|
158
|
+
message: string; // Human-readable message
|
|
159
|
+
details?: unknown; // Additional context
|
|
160
|
+
requestId?: string; // For debugging
|
|
161
|
+
documentation?: string; // Link to docs
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// HTTP status codes mapping
|
|
165
|
+
const ERROR_STATUS_MAP: Record<string, number> = {
|
|
166
|
+
VALIDATION_ERROR: 400,
|
|
167
|
+
UNAUTHORIZED: 401,
|
|
168
|
+
FORBIDDEN: 403,
|
|
169
|
+
NOT_FOUND: 404,
|
|
170
|
+
CONFLICT: 409,
|
|
171
|
+
RATE_LIMITED: 429,
|
|
172
|
+
INTERNAL_ERROR: 500,
|
|
173
|
+
SERVICE_UNAVAILABLE: 503,
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
// Error class hierarchy
|
|
177
|
+
class APIException extends Error {
|
|
178
|
+
constructor(
|
|
179
|
+
public code: string,
|
|
180
|
+
message: string,
|
|
181
|
+
public details?: unknown,
|
|
182
|
+
public statusCode: number = ERROR_STATUS_MAP[code] || 500
|
|
183
|
+
) {
|
|
184
|
+
super(message);
|
|
185
|
+
this.name = 'APIException';
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
toJSON(): APIError {
|
|
189
|
+
return {
|
|
190
|
+
code: this.code,
|
|
191
|
+
message: this.message,
|
|
192
|
+
details: this.details,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
class ValidationException extends APIException {
|
|
198
|
+
constructor(errors: z.ZodError) {
|
|
199
|
+
super(
|
|
200
|
+
'VALIDATION_ERROR',
|
|
201
|
+
'Request validation failed',
|
|
202
|
+
errors.errors.map(e => ({
|
|
203
|
+
field: e.path.join('.'),
|
|
204
|
+
message: e.message,
|
|
205
|
+
code: e.code,
|
|
206
|
+
})),
|
|
207
|
+
400
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
class NotFoundException extends APIException {
|
|
213
|
+
constructor(resource: string, id: string) {
|
|
214
|
+
super(
|
|
215
|
+
'NOT_FOUND',
|
|
216
|
+
`${resource} not found`,
|
|
217
|
+
{ resource, id },
|
|
218
|
+
404
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Global error handler
|
|
224
|
+
function errorHandler(
|
|
225
|
+
err: Error,
|
|
226
|
+
req: express.Request,
|
|
227
|
+
res: express.Response,
|
|
228
|
+
next: express.NextFunction
|
|
229
|
+
) {
|
|
230
|
+
const requestId = req.headers['x-request-id'] as string;
|
|
231
|
+
|
|
232
|
+
// Log error
|
|
233
|
+
logger.error({
|
|
234
|
+
requestId,
|
|
235
|
+
error: err.message,
|
|
236
|
+
stack: err.stack,
|
|
237
|
+
path: req.path,
|
|
238
|
+
method: req.method,
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
if (err instanceof APIException) {
|
|
242
|
+
return res.status(err.statusCode).json({
|
|
243
|
+
error: {
|
|
244
|
+
...err.toJSON(),
|
|
245
|
+
requestId,
|
|
246
|
+
},
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (err instanceof z.ZodError) {
|
|
251
|
+
return res.status(400).json({
|
|
252
|
+
error: new ValidationException(err).toJSON(),
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Internal errors - don't leak details
|
|
257
|
+
res.status(500).json({
|
|
258
|
+
error: {
|
|
259
|
+
code: 'INTERNAL_ERROR',
|
|
260
|
+
message: 'An unexpected error occurred',
|
|
261
|
+
requestId,
|
|
262
|
+
},
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### 3. API Versioning
|
|
268
|
+
|
|
269
|
+
```typescript
|
|
270
|
+
// URL versioning (recommended)
|
|
271
|
+
// /api/v1/users
|
|
272
|
+
// /api/v2/users
|
|
273
|
+
|
|
274
|
+
// Version router
|
|
275
|
+
const v1Router = express.Router();
|
|
276
|
+
const v2Router = express.Router();
|
|
277
|
+
|
|
278
|
+
// V1 response format
|
|
279
|
+
v1Router.get('/users/:id', async (req, res) => {
|
|
280
|
+
const user = await userService.findById(req.params.id);
|
|
281
|
+
res.json(user); // Direct response
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
// V2 response format (with envelope)
|
|
285
|
+
v2Router.get('/users/:id', async (req, res) => {
|
|
286
|
+
const user = await userService.findById(req.params.id);
|
|
287
|
+
res.json({
|
|
288
|
+
data: user,
|
|
289
|
+
meta: { version: 'v2' },
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
app.use('/api/v1', v1Router);
|
|
294
|
+
app.use('/api/v2', v2Router);
|
|
295
|
+
|
|
296
|
+
// Header versioning alternative
|
|
297
|
+
function versionMiddleware(req: Request, res: Response, next: NextFunction) {
|
|
298
|
+
const version = req.headers['api-version'] || req.headers['accept-version'] || 'v1';
|
|
299
|
+
req.apiVersion = version;
|
|
300
|
+
next();
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Content negotiation
|
|
304
|
+
app.get('/users/:id', (req, res) => {
|
|
305
|
+
const user = await userService.findById(req.params.id);
|
|
306
|
+
|
|
307
|
+
if (req.apiVersion === 'v2') {
|
|
308
|
+
return res.json({ data: user });
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
res.json(user);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
// Sunset header for deprecation
|
|
315
|
+
router.use('/v1/*', (req, res, next) => {
|
|
316
|
+
res.set('Sunset', 'Sat, 31 Dec 2025 23:59:59 GMT');
|
|
317
|
+
res.set('Deprecation', 'true');
|
|
318
|
+
res.set('Link', '</api/v2>; rel="successor-version"');
|
|
319
|
+
next();
|
|
320
|
+
});
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
### 4. Rate Limiting
|
|
324
|
+
|
|
325
|
+
```typescript
|
|
326
|
+
import rateLimit from 'express-rate-limit';
|
|
327
|
+
import RedisStore from 'rate-limit-redis';
|
|
328
|
+
import Redis from 'ioredis';
|
|
329
|
+
|
|
330
|
+
const redis = new Redis(process.env.REDIS_URL);
|
|
331
|
+
|
|
332
|
+
// Basic rate limiter
|
|
333
|
+
const basicLimiter = rateLimit({
|
|
334
|
+
windowMs: 60 * 1000, // 1 minute
|
|
335
|
+
max: 100, // 100 requests per minute
|
|
336
|
+
standardHeaders: true, // Return rate limit info in headers
|
|
337
|
+
legacyHeaders: false,
|
|
338
|
+
store: new RedisStore({
|
|
339
|
+
sendCommand: (...args: string[]) => redis.call(...args),
|
|
340
|
+
}),
|
|
341
|
+
handler: (req, res) => {
|
|
342
|
+
res.status(429).json({
|
|
343
|
+
error: {
|
|
344
|
+
code: 'RATE_LIMITED',
|
|
345
|
+
message: 'Too many requests',
|
|
346
|
+
retryAfter: res.getHeader('Retry-After'),
|
|
347
|
+
},
|
|
348
|
+
});
|
|
349
|
+
},
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
// Tiered rate limiting based on subscription
|
|
353
|
+
function createTieredLimiter(tier: 'free' | 'pro' | 'enterprise') {
|
|
354
|
+
const limits = {
|
|
355
|
+
free: { windowMs: 60000, max: 60 },
|
|
356
|
+
pro: { windowMs: 60000, max: 600 },
|
|
357
|
+
enterprise: { windowMs: 60000, max: 6000 },
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
return rateLimit({
|
|
361
|
+
...limits[tier],
|
|
362
|
+
keyGenerator: (req) => `${tier}:${req.user?.id || req.ip}`,
|
|
363
|
+
store: new RedisStore({ sendCommand: (...args) => redis.call(...args) }),
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Per-endpoint rate limiting
|
|
368
|
+
const strictLimiter = rateLimit({
|
|
369
|
+
windowMs: 60 * 1000,
|
|
370
|
+
max: 10,
|
|
371
|
+
message: { error: { code: 'RATE_LIMITED', message: 'Rate limit exceeded for this endpoint' } },
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
router.post('/auth/login', strictLimiter, loginHandler);
|
|
375
|
+
|
|
376
|
+
// Sliding window with Redis
|
|
377
|
+
async function slidingWindowRateLimit(
|
|
378
|
+
key: string,
|
|
379
|
+
limit: number,
|
|
380
|
+
windowSeconds: number
|
|
381
|
+
): Promise<{ allowed: boolean; remaining: number; resetAt: number }> {
|
|
382
|
+
const now = Date.now();
|
|
383
|
+
const windowStart = now - windowSeconds * 1000;
|
|
384
|
+
|
|
385
|
+
const multi = redis.multi();
|
|
386
|
+
|
|
387
|
+
// Remove old entries
|
|
388
|
+
multi.zremrangebyscore(key, 0, windowStart);
|
|
389
|
+
// Add current request
|
|
390
|
+
multi.zadd(key, now.toString(), `${now}-${Math.random()}`);
|
|
391
|
+
// Count requests in window
|
|
392
|
+
multi.zcard(key);
|
|
393
|
+
// Set expiry
|
|
394
|
+
multi.expire(key, windowSeconds);
|
|
395
|
+
|
|
396
|
+
const results = await multi.exec();
|
|
397
|
+
const count = results?.[2]?.[1] as number;
|
|
398
|
+
|
|
399
|
+
return {
|
|
400
|
+
allowed: count <= limit,
|
|
401
|
+
remaining: Math.max(0, limit - count),
|
|
402
|
+
resetAt: Math.ceil((windowStart + windowSeconds * 1000) / 1000),
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
### 5. GraphQL Schema Design
|
|
408
|
+
|
|
409
|
+
```typescript
|
|
410
|
+
import { makeExecutableSchema } from '@graphql-tools/schema';
|
|
411
|
+
|
|
412
|
+
const typeDefs = `#graphql
|
|
413
|
+
type Query {
|
|
414
|
+
user(id: ID!): User
|
|
415
|
+
users(
|
|
416
|
+
first: Int
|
|
417
|
+
after: String
|
|
418
|
+
filter: UserFilter
|
|
419
|
+
orderBy: UserOrderBy
|
|
420
|
+
): UserConnection!
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
type Mutation {
|
|
424
|
+
createUser(input: CreateUserInput!): CreateUserPayload!
|
|
425
|
+
updateUser(id: ID!, input: UpdateUserInput!): UpdateUserPayload!
|
|
426
|
+
deleteUser(id: ID!): DeleteUserPayload!
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
# Relay-style pagination
|
|
430
|
+
type UserConnection {
|
|
431
|
+
edges: [UserEdge!]!
|
|
432
|
+
pageInfo: PageInfo!
|
|
433
|
+
totalCount: Int!
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
type UserEdge {
|
|
437
|
+
cursor: String!
|
|
438
|
+
node: User!
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
type PageInfo {
|
|
442
|
+
hasNextPage: Boolean!
|
|
443
|
+
hasPreviousPage: Boolean!
|
|
444
|
+
startCursor: String
|
|
445
|
+
endCursor: String
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
type User {
|
|
449
|
+
id: ID!
|
|
450
|
+
email: String!
|
|
451
|
+
name: String!
|
|
452
|
+
status: UserStatus!
|
|
453
|
+
createdAt: DateTime!
|
|
454
|
+
updatedAt: DateTime!
|
|
455
|
+
posts(first: Int, after: String): PostConnection!
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
enum UserStatus {
|
|
459
|
+
ACTIVE
|
|
460
|
+
INACTIVE
|
|
461
|
+
SUSPENDED
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
input UserFilter {
|
|
465
|
+
status: UserStatus
|
|
466
|
+
search: String
|
|
467
|
+
createdAfter: DateTime
|
|
468
|
+
createdBefore: DateTime
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
input UserOrderBy {
|
|
472
|
+
field: UserOrderField!
|
|
473
|
+
direction: OrderDirection!
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
enum UserOrderField {
|
|
477
|
+
CREATED_AT
|
|
478
|
+
NAME
|
|
479
|
+
EMAIL
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
enum OrderDirection {
|
|
483
|
+
ASC
|
|
484
|
+
DESC
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
# Input types for mutations
|
|
488
|
+
input CreateUserInput {
|
|
489
|
+
email: String!
|
|
490
|
+
name: String!
|
|
491
|
+
password: String!
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
# Payload types for mutations
|
|
495
|
+
type CreateUserPayload {
|
|
496
|
+
user: User
|
|
497
|
+
errors: [UserError!]
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
type UserError {
|
|
501
|
+
field: String!
|
|
502
|
+
message: String!
|
|
503
|
+
code: String!
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
scalar DateTime
|
|
507
|
+
`;
|
|
508
|
+
|
|
509
|
+
const resolvers = {
|
|
510
|
+
Query: {
|
|
511
|
+
user: async (_, { id }, ctx) => {
|
|
512
|
+
return ctx.loaders.user.load(id);
|
|
513
|
+
},
|
|
514
|
+
|
|
515
|
+
users: async (_, args, ctx) => {
|
|
516
|
+
const { first = 20, after, filter, orderBy } = args;
|
|
517
|
+
|
|
518
|
+
const { users, total, hasMore } = await userService.list({
|
|
519
|
+
limit: first,
|
|
520
|
+
cursor: after ? decodeCursor(after) : undefined,
|
|
521
|
+
filter,
|
|
522
|
+
orderBy,
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
const edges = users.map(user => ({
|
|
526
|
+
cursor: encodeCursor(user.id),
|
|
527
|
+
node: user,
|
|
528
|
+
}));
|
|
529
|
+
|
|
530
|
+
return {
|
|
531
|
+
edges,
|
|
532
|
+
totalCount: total,
|
|
533
|
+
pageInfo: {
|
|
534
|
+
hasNextPage: hasMore,
|
|
535
|
+
hasPreviousPage: !!after,
|
|
536
|
+
startCursor: edges[0]?.cursor,
|
|
537
|
+
endCursor: edges[edges.length - 1]?.cursor,
|
|
538
|
+
},
|
|
539
|
+
};
|
|
540
|
+
},
|
|
541
|
+
},
|
|
542
|
+
|
|
543
|
+
Mutation: {
|
|
544
|
+
createUser: async (_, { input }, ctx) => {
|
|
545
|
+
try {
|
|
546
|
+
const user = await userService.create(input);
|
|
547
|
+
return { user, errors: [] };
|
|
548
|
+
} catch (error) {
|
|
549
|
+
return {
|
|
550
|
+
user: null,
|
|
551
|
+
errors: [{ field: 'email', message: error.message, code: 'VALIDATION_ERROR' }],
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
},
|
|
555
|
+
},
|
|
556
|
+
|
|
557
|
+
User: {
|
|
558
|
+
posts: async (user, args, ctx) => {
|
|
559
|
+
return ctx.loaders.userPosts.load({ userId: user.id, ...args });
|
|
560
|
+
},
|
|
561
|
+
},
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
// DataLoader for N+1 prevention
|
|
565
|
+
import DataLoader from 'dataloader';
|
|
566
|
+
|
|
567
|
+
function createLoaders() {
|
|
568
|
+
return {
|
|
569
|
+
user: new DataLoader(async (ids: string[]) => {
|
|
570
|
+
const users = await userService.findByIds(ids);
|
|
571
|
+
return ids.map(id => users.find(u => u.id === id));
|
|
572
|
+
}),
|
|
573
|
+
|
|
574
|
+
userPosts: new DataLoader(async (keys) => {
|
|
575
|
+
// Batch load posts for multiple users
|
|
576
|
+
const userIds = keys.map(k => k.userId);
|
|
577
|
+
const posts = await postService.findByUserIds(userIds);
|
|
578
|
+
|
|
579
|
+
return keys.map(key =>
|
|
580
|
+
posts.filter(p => p.userId === key.userId)
|
|
581
|
+
);
|
|
582
|
+
}),
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
### 6. OpenAPI Specification
|
|
588
|
+
|
|
589
|
+
```yaml
|
|
590
|
+
openapi: 3.1.0
|
|
591
|
+
info:
|
|
592
|
+
title: User API
|
|
593
|
+
version: 1.0.0
|
|
594
|
+
description: User management API
|
|
595
|
+
contact:
|
|
596
|
+
email: api@example.com
|
|
597
|
+
license:
|
|
598
|
+
name: MIT
|
|
599
|
+
|
|
600
|
+
servers:
|
|
601
|
+
- url: https://api.example.com/v1
|
|
602
|
+
description: Production
|
|
603
|
+
- url: https://staging-api.example.com/v1
|
|
604
|
+
description: Staging
|
|
605
|
+
|
|
606
|
+
paths:
|
|
607
|
+
/users:
|
|
608
|
+
get:
|
|
609
|
+
summary: List users
|
|
610
|
+
operationId: listUsers
|
|
611
|
+
tags: [Users]
|
|
612
|
+
parameters:
|
|
613
|
+
- name: page
|
|
614
|
+
in: query
|
|
615
|
+
schema:
|
|
616
|
+
type: integer
|
|
617
|
+
minimum: 1
|
|
618
|
+
default: 1
|
|
619
|
+
- name: limit
|
|
620
|
+
in: query
|
|
621
|
+
schema:
|
|
622
|
+
type: integer
|
|
623
|
+
minimum: 1
|
|
624
|
+
maximum: 100
|
|
625
|
+
default: 20
|
|
626
|
+
- name: status
|
|
627
|
+
in: query
|
|
628
|
+
schema:
|
|
629
|
+
$ref: '#/components/schemas/UserStatus'
|
|
630
|
+
responses:
|
|
631
|
+
'200':
|
|
632
|
+
description: Successful response
|
|
633
|
+
content:
|
|
634
|
+
application/json:
|
|
635
|
+
schema:
|
|
636
|
+
type: object
|
|
637
|
+
properties:
|
|
638
|
+
data:
|
|
639
|
+
type: array
|
|
640
|
+
items:
|
|
641
|
+
$ref: '#/components/schemas/User'
|
|
642
|
+
pagination:
|
|
643
|
+
$ref: '#/components/schemas/Pagination'
|
|
644
|
+
'400':
|
|
645
|
+
$ref: '#/components/responses/BadRequest'
|
|
646
|
+
'401':
|
|
647
|
+
$ref: '#/components/responses/Unauthorized'
|
|
648
|
+
|
|
649
|
+
post:
|
|
650
|
+
summary: Create user
|
|
651
|
+
operationId: createUser
|
|
652
|
+
tags: [Users]
|
|
653
|
+
requestBody:
|
|
654
|
+
required: true
|
|
655
|
+
content:
|
|
656
|
+
application/json:
|
|
657
|
+
schema:
|
|
658
|
+
$ref: '#/components/schemas/CreateUserInput'
|
|
659
|
+
responses:
|
|
660
|
+
'201':
|
|
661
|
+
description: User created
|
|
662
|
+
headers:
|
|
663
|
+
Location:
|
|
664
|
+
schema:
|
|
665
|
+
type: string
|
|
666
|
+
content:
|
|
667
|
+
application/json:
|
|
668
|
+
schema:
|
|
669
|
+
type: object
|
|
670
|
+
properties:
|
|
671
|
+
data:
|
|
672
|
+
$ref: '#/components/schemas/User'
|
|
673
|
+
|
|
674
|
+
components:
|
|
675
|
+
schemas:
|
|
676
|
+
User:
|
|
677
|
+
type: object
|
|
678
|
+
required: [id, email, name, status, createdAt]
|
|
679
|
+
properties:
|
|
680
|
+
id:
|
|
681
|
+
type: string
|
|
682
|
+
format: uuid
|
|
683
|
+
email:
|
|
684
|
+
type: string
|
|
685
|
+
format: email
|
|
686
|
+
name:
|
|
687
|
+
type: string
|
|
688
|
+
status:
|
|
689
|
+
$ref: '#/components/schemas/UserStatus'
|
|
690
|
+
createdAt:
|
|
691
|
+
type: string
|
|
692
|
+
format: date-time
|
|
693
|
+
|
|
694
|
+
UserStatus:
|
|
695
|
+
type: string
|
|
696
|
+
enum: [active, inactive, suspended]
|
|
697
|
+
|
|
698
|
+
CreateUserInput:
|
|
699
|
+
type: object
|
|
700
|
+
required: [email, name, password]
|
|
701
|
+
properties:
|
|
702
|
+
email:
|
|
703
|
+
type: string
|
|
704
|
+
format: email
|
|
705
|
+
name:
|
|
706
|
+
type: string
|
|
707
|
+
minLength: 2
|
|
708
|
+
maxLength: 100
|
|
709
|
+
password:
|
|
710
|
+
type: string
|
|
711
|
+
minLength: 8
|
|
712
|
+
|
|
713
|
+
Pagination:
|
|
714
|
+
type: object
|
|
715
|
+
properties:
|
|
716
|
+
page:
|
|
717
|
+
type: integer
|
|
718
|
+
limit:
|
|
719
|
+
type: integer
|
|
720
|
+
total:
|
|
721
|
+
type: integer
|
|
722
|
+
totalPages:
|
|
723
|
+
type: integer
|
|
724
|
+
hasMore:
|
|
725
|
+
type: boolean
|
|
726
|
+
|
|
727
|
+
Error:
|
|
728
|
+
type: object
|
|
729
|
+
required: [code, message]
|
|
730
|
+
properties:
|
|
731
|
+
code:
|
|
732
|
+
type: string
|
|
733
|
+
message:
|
|
734
|
+
type: string
|
|
735
|
+
details:
|
|
736
|
+
type: object
|
|
737
|
+
|
|
738
|
+
responses:
|
|
739
|
+
BadRequest:
|
|
740
|
+
description: Bad request
|
|
741
|
+
content:
|
|
742
|
+
application/json:
|
|
743
|
+
schema:
|
|
744
|
+
type: object
|
|
745
|
+
properties:
|
|
746
|
+
error:
|
|
747
|
+
$ref: '#/components/schemas/Error'
|
|
748
|
+
|
|
749
|
+
Unauthorized:
|
|
750
|
+
description: Unauthorized
|
|
751
|
+
content:
|
|
752
|
+
application/json:
|
|
753
|
+
schema:
|
|
754
|
+
type: object
|
|
755
|
+
properties:
|
|
756
|
+
error:
|
|
757
|
+
$ref: '#/components/schemas/Error'
|
|
758
|
+
|
|
759
|
+
securitySchemes:
|
|
760
|
+
bearerAuth:
|
|
761
|
+
type: http
|
|
762
|
+
scheme: bearer
|
|
763
|
+
bearerFormat: JWT
|
|
764
|
+
|
|
765
|
+
security:
|
|
766
|
+
- bearerAuth: []
|
|
767
|
+
```
|
|
768
|
+
|
|
769
|
+
## Use Cases
|
|
770
|
+
|
|
771
|
+
### 1. Public API Design
|
|
772
|
+
|
|
773
|
+
```typescript
|
|
774
|
+
// Design for external developers
|
|
775
|
+
router.get('/products', async (req, res) => {
|
|
776
|
+
// Always include request ID for support
|
|
777
|
+
const requestId = req.headers['x-request-id'] || generateRequestId();
|
|
778
|
+
res.set('X-Request-ID', requestId);
|
|
779
|
+
|
|
780
|
+
// Rate limit headers
|
|
781
|
+
res.set('X-RateLimit-Limit', '1000');
|
|
782
|
+
res.set('X-RateLimit-Remaining', String(remaining));
|
|
783
|
+
res.set('X-RateLimit-Reset', String(resetTime));
|
|
784
|
+
|
|
785
|
+
// Response
|
|
786
|
+
res.json({
|
|
787
|
+
data: products,
|
|
788
|
+
pagination: { ... },
|
|
789
|
+
meta: {
|
|
790
|
+
requestId,
|
|
791
|
+
apiVersion: 'v1',
|
|
792
|
+
},
|
|
793
|
+
});
|
|
794
|
+
});
|
|
795
|
+
```
|
|
796
|
+
|
|
797
|
+
### 2. Internal Microservice API
|
|
798
|
+
|
|
799
|
+
```typescript
|
|
800
|
+
// gRPC for internal services
|
|
801
|
+
// proto/user.proto
|
|
802
|
+
syntax = "proto3";
|
|
803
|
+
|
|
804
|
+
package user;
|
|
805
|
+
|
|
806
|
+
service UserService {
|
|
807
|
+
rpc GetUser(GetUserRequest) returns (User);
|
|
808
|
+
rpc ListUsers(ListUsersRequest) returns (ListUsersResponse);
|
|
809
|
+
rpc CreateUser(CreateUserRequest) returns (User);
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
message User {
|
|
813
|
+
string id = 1;
|
|
814
|
+
string email = 2;
|
|
815
|
+
string name = 3;
|
|
816
|
+
UserStatus status = 4;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
enum UserStatus {
|
|
820
|
+
UNKNOWN = 0;
|
|
821
|
+
ACTIVE = 1;
|
|
822
|
+
INACTIVE = 2;
|
|
823
|
+
}
|
|
824
|
+
```
|
|
825
|
+
|
|
826
|
+
## Best Practices
|
|
827
|
+
|
|
828
|
+
### Do's
|
|
829
|
+
|
|
830
|
+
- **Use consistent naming** - Plural nouns for collections
|
|
831
|
+
- **Return appropriate status codes** - 201 for create, 204 for delete
|
|
832
|
+
- **Include request IDs** - For debugging and support
|
|
833
|
+
- **Document everything** - OpenAPI/Swagger specs
|
|
834
|
+
- **Version from day one** - Avoid breaking changes
|
|
835
|
+
- **Implement idempotency** - For POST/PUT operations
|
|
836
|
+
|
|
837
|
+
### Don'ts
|
|
838
|
+
|
|
839
|
+
- Don't use verbs in URLs
|
|
840
|
+
- Don't return 200 for errors
|
|
841
|
+
- Don't expose internal errors
|
|
842
|
+
- Don't skip pagination
|
|
843
|
+
- Don't ignore cache headers
|
|
844
|
+
- Don't forget rate limiting
|
|
845
|
+
|
|
846
|
+
## Related Skills
|
|
847
|
+
|
|
848
|
+
- **backend-development** - Implementation patterns
|
|
849
|
+
- **security** - API security
|
|
850
|
+
- **caching-strategies** - Response caching
|
|
851
|
+
|
|
852
|
+
## Reference Resources
|
|
853
|
+
|
|
854
|
+
- [REST API Design](https://restfulapi.net/)
|
|
855
|
+
- [GraphQL Best Practices](https://graphql.org/learn/best-practices/)
|
|
856
|
+
- [Google API Design Guide](https://cloud.google.com/apis/design)
|
|
857
|
+
- [Microsoft REST Guidelines](https://github.com/microsoft/api-guidelines)
|