[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6NT82XB4MPHMQSSM0JSQ2XF":3,"skill-same:01M6NT82XB4MPHMQSSM0JSQ2XF":75},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":31,"publishedAt":48,"linkOnly":49,"markdownContent":50,"markdownBody":51,"frontMatter":52,"refName":53,"commitSha":54,"skillDirectory":55,"forksCount":14,"repositoryHtmlUrl":56,"repositoryHomepageUrl":6,"ownerHtmlUrl":57,"ownerBlogUrl":58,"ownerDescription":59,"updatedAt":60,"install":61,"resources":74},"01M6NT82XB4MPHMQSSM0JSQ2XF","api-patterns",null,"API design, versioning, testing, schema validation, and contract testing patterns for REST and GraphQL APIs.","rubicanjr\u002FFinCognis","rubicanjr","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F234912519?v=4","01M6NT156NND8J474G34SB3923","Rubi","01M6NT15B70RWXE1N3YV0BZY8A",0,"MIT","https:\u002F\u002Fgithub.com\u002Frubicanjr\u002FFinCognis\u002Fblob\u002Fmain\u002Fskills\u002Fapi-patterns\u002FSKILL.md",[18,23,26],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000008","测试与质量","SCENARIO","使用场景",{"categoryNo":24,"categoryName":25,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000010","后端与接口",{"categoryNo":27,"categoryName":28,"taxonomyType":29,"taxonomyTypeDesc":30},"01JBSEEDCAT000000000000030","开发工程师","PROFESSION","适用职业",[32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47],"api patterns","api","patterns","testing","design","api design","validation","REST","GraphQL","testing patterns","versioning","schema","contract","contract testing","schema validation","for","2026-11-10 10:12:30",false,"---\nname: api-patterns\ndescription: API design, versioning, testing, schema validation, and contract testing patterns for REST and GraphQL APIs.\n---\n\n# API Patterns\n\nREST and GraphQL API design patterns for consistent, versioned, and well-tested interfaces.\n\n## API Versioning\n\n### URL-Based Versioning\n\n```typescript\n\u002F\u002F routes\u002Fv1\u002Fmarkets.ts\n\u002F\u002F routes\u002Fv2\u002Fmarkets.ts\n\u002F\u002F URL: \u002Fapi\u002Fv1\u002Fmarkets, \u002Fapi\u002Fv2\u002Fmarkets\n\n\u002F\u002F Express router setup\nimport { Router } from 'express'\n\nconst v1Router = Router()\nconst v2Router = Router()\n\napp.use('\u002Fapi\u002Fv1', v1Router)\napp.use('\u002Fapi\u002Fv2', v2Router)\n\n\u002F\u002F Deprecation header middleware\nfunction deprecationWarning(version: string, sunsetDate: string) {\n  return (_req: Request, res: Response, next: NextFunction) => {\n    res.setHeader('Deprecation', 'true')\n    res.setHeader('Sunset', sunsetDate)\n    res.setHeader('Link', `\u003C\u002Fapi\u002Fv${parseInt(version) + 1}>; rel=\"successor-version\"`)\n    next()\n  }\n}\n\nv1Router.use(deprecationWarning('1', 'Sat, 01 Jan 2027 00:00:00 GMT'))\n```\n\n### Header-Based Versioning\n\n```typescript\n\u002F\u002F Accept: application\u002Fvnd.api+json;version=2\nfunction versionMiddleware(req: Request, res: Response, next: NextFunction) {\n  const accept = req.headers['accept'] || ''\n  const match = accept.match(\u002Fversion=(\\d+)\u002F)\n  req.apiVersion = match ? parseInt(match[1]) : 1\n  next()\n}\n```\n\n## Schema Validation with Zod\n\n### Request + Response Validation\n\n```typescript\nimport { z } from 'zod'\n\n\u002F\u002F Request schema\nconst CreateMarketSchema = z.object({\n  name: z.string().min(1).max(200),\n  description: z.string().max(2000).optional(),\n  category: z.enum(['sports', 'politics', 'crypto', 'tech']),\n  closeAt: z.string().datetime(),\n  initialLiquidity: z.number().positive().max(1_000_000)\n})\n\n\u002F\u002F Response schema - strip internal fields\nconst MarketResponseSchema = z.object({\n  id: z.string().uuid(),\n  name: z.string(),\n  category: z.string(),\n  status: z.enum(['open', 'closed', 'resolved']),\n  volume: z.number(),\n  createdAt: z.string().datetime()\n})\n\ntype CreateMarketDto = z.infer\u003Ctypeof CreateMarketSchema>\ntype MarketResponse = z.infer\u003Ctypeof MarketResponseSchema>\n\n\u002F\u002F Validation middleware\nfunction validate\u003CT>(schema: z.ZodSchema\u003CT>) {\n  return (req: Request, res: Response, next: NextFunction) => {\n    const result = schema.safeParse(req.body)\n    if (!result.success) {\n      return res.status(400).json({\n        success: false,\n        error: 'Validation failed',\n        code: 'VALIDATION_ERROR',\n        details: result.error.flatten()\n      })\n    }\n    req.validated = result.data\n    next()\n  }\n}\n\n\u002F\u002F Usage\nrouter.post('\u002Fmarkets', validate(CreateMarketSchema), async (req, res) => {\n  const dto = req.validated as CreateMarketDto\n  const market = await marketService.create(dto)\n  const response = MarketResponseSchema.parse(market)\n  res.status(201).json({ success: true, data: response })\n})\n```\n\n## Standardized Error Responses\n\n```typescript\n\u002F\u002F Always: { success, error, code, details? }\ninterface ApiError {\n  success: false\n  error: string\n  code: string\n  details?: unknown\n  requestId?: string\n}\n\ninterface ApiSuccess\u003CT> {\n  success: true\n  data: T\n  meta?: { total?: number; page?: number; limit?: number }\n}\n\nconst ERROR_CODES = {\n  VALIDATION_ERROR: 400,\n  UNAUTHORIZED: 401,\n  FORBIDDEN: 403,\n  NOT_FOUND: 404,\n  CONFLICT: 409,\n  RATE_LIMITED: 429,\n  INTERNAL: 500\n} as const\n\nfunction apiError(\n  res: Response,\n  code: keyof typeof ERROR_CODES,\n  message: string,\n  details?: unknown\n): Response {\n  return res.status(ERROR_CODES[code]).json({\n    success: false,\n    error: message,\n    code,\n    details,\n    requestId: res.locals.requestId\n  } satisfies ApiError)\n}\n```\n\n## Pagination Patterns\n\n### Cursor-Based Pagination (Recommended for large datasets)\n\n```typescript\ninterface CursorPage\u003CT> {\n  items: T[]\n  nextCursor: string | null\n  prevCursor: string | null\n  hasMore: boolean\n}\n\nasync function paginateWithCursor\u003CT extends { id: string; createdAt: Date }>(\n  query: (cursor: string | null, limit: number) => Promise\u003CT[]>,\n  cursor: string | null,\n  limit = 20\n): Promise\u003CCursorPage\u003CT>> {\n  \u002F\u002F Fetch one extra to detect hasMore\n  const items = await query(cursor, limit + 1)\n  const hasMore = items.length > limit\n  const page = hasMore ? items.slice(0, limit) : items\n\n  return {\n    items: page,\n    nextCursor: hasMore ? Buffer.from(page[page.length - 1].id).toString('base64') : null,\n    prevCursor: cursor,\n    hasMore\n  }\n}\n\n\u002F\u002F GET \u002Fapi\u002Fmarkets?cursor=\u003Cbase64>&limit=20\nrouter.get('\u002Fmarkets', async (req, res) => {\n  const cursor = req.query.cursor as string | null\n  const limit = Math.min(parseInt(req.query.limit as string) || 20, 100)\n  const decoded = cursor ? Buffer.from(cursor, 'base64').toString() : null\n\n  const page = await paginateWithCursor(\n    (c, l) => db.market.findMany({\n      take: l,\n      skip: c ? 1 : 0,\n      cursor: c ? { id: c } : undefined,\n      orderBy: { createdAt: 'desc' }\n    }),\n    decoded,\n    limit\n  )\n\n  res.json({ success: true, ...page })\n})\n```\n\n### Offset Pagination (Simple use cases)\n\n```typescript\ninterface OffsetPage\u003CT> {\n  items: T[]\n  total: number\n  page: number\n  limit: number\n  totalPages: number\n}\n\n\u002F\u002F GET \u002Fapi\u002Fmarkets?page=2&limit=20\n```\n\n## Rate Limiting\n\n### Token Bucket with Redis\n\n```typescript\nimport Redis from 'ioredis'\n\nconst redis = new Redis(process.env.REDIS_URL!)\n\nasync function tokenBucket(\n  key: string,\n  capacity: number,\n  refillRate: number   \u002F\u002F tokens per second\n): Promise\u003C{ allowed: boolean; remaining: number; resetIn: number }> {\n  const now = Date.now()\n  const bucketKey = `ratelimit:${key}`\n\n  const script = `\n    local key = KEYS[1]\n    local capacity = tonumber(ARGV[1])\n    local refill_rate = tonumber(ARGV[2])\n    local now = tonumber(ARGV[3])\n\n    local bucket = redis.call('HMGET', key, 'tokens', 'last_refill')\n    local tokens = tonumber(bucket[1]) or capacity\n    local last_refill = tonumber(bucket[2]) or now\n\n    local elapsed = (now - last_refill) \u002F 1000\n    tokens = math.min(capacity, tokens + elapsed * refill_rate)\n\n    if tokens >= 1 then\n      tokens = tokens - 1\n      redis.call('HMSET', key, 'tokens', tokens, 'last_refill', now)\n      redis.call('EXPIRE', key, math.ceil(capacity \u002F refill_rate) + 1)\n      return {1, math.floor(tokens)}\n    else\n      return {0, 0}\n    end\n  `\n\n  const [allowed, remaining] = await redis.eval(\n    script, 1, bucketKey, capacity, refillRate, now\n  ) as [number, number]\n\n  return {\n    allowed: allowed === 1,\n    remaining,\n    resetIn: allowed ? 0 : Math.ceil(1 \u002F refillRate)\n  }\n}\n\nfunction rateLimitMiddleware(capacity: number, refillRate: number) {\n  return async (req: Request, res: Response, next: NextFunction) => {\n    const key = req.user?.id || req.ip || 'anonymous'\n    const result = await tokenBucket(key, capacity, refillRate)\n\n    res.setHeader('X-RateLimit-Limit', capacity)\n    res.setHeader('X-RateLimit-Remaining', result.remaining)\n\n    if (!result.allowed) {\n      res.setHeader('Retry-After', result.resetIn)\n      return apiError(res, 'RATE_LIMITED', 'Too many requests')\n    }\n    next()\n  }\n}\n```\n\n## API Endpoint Testing\n\n```typescript\nimport { describe, it, expect, beforeAll, afterAll } from 'vitest'\nimport supertest from 'supertest'\nimport { app } from '..\u002Fapp'\nimport { db } from '..\u002Fdb'\n\nconst request = supertest(app)\n\ndescribe('POST \u002Fapi\u002Fv1\u002Fmarkets', () => {\n  let authToken: string\n\n  beforeAll(async () => {\n    authToken = await getTestToken()\n  })\n\n  it('creates a market with valid payload', async () => {\n    const payload = {\n      name: 'Will BTC reach 100k?',\n      category: 'crypto',\n      closeAt: new Date(Date.now() + 86400000).toISOString(),\n      initialLiquidity: 1000\n    }\n\n    const res = await request\n      .post('\u002Fapi\u002Fv1\u002Fmarkets')\n      .set('Authorization', `Bearer ${authToken}`)\n      .send(payload)\n      .expect(201)\n\n    expect(res.body.success).toBe(true)\n    expect(res.body.data).toMatchObject({\n      id: expect.any(String),\n      name: payload.name,\n      status: 'open'\n    })\n  })\n\n  it('rejects invalid payload with 400', async () => {\n    const res = await request\n      .post('\u002Fapi\u002Fv1\u002Fmarkets')\n      .set('Authorization', `Bearer ${authToken}`)\n      .send({ name: '' })   \u002F\u002F invalid\n      .expect(400)\n\n    expect(res.body.success).toBe(false)\n    expect(res.body.code).toBe('VALIDATION_ERROR')\n  })\n\n  it('returns 401 without auth token', async () => {\n    const res = await request.post('\u002Fapi\u002Fv1\u002Fmarkets').send({}).expect(401)\n    expect(res.body.code).toBe('UNAUTHORIZED')\n  })\n})\n```\n\n## Breaking Change Detection Checklist\n\n```\nBefore releasing a new API version, verify:\n\nBREAKING changes (require version bump):\n  [ ] Removed a field from response\n  [ ] Changed field type (string → number)\n  [ ] Renamed a field\n  [ ] Changed HTTP method\n  [ ] Removed an endpoint\n  [ ] Changed required → optional (ok) vs optional → required (breaking)\n  [ ] Changed error codes\u002Fformat\n\nNON-BREAKING changes (safe to ship):\n  [ ] Added new optional fields to response\n  [ ] Added new optional query parameters\n  [ ] Added new endpoints\n  [ ] Added new enum values (check client handling)\n  [ ] Relaxed validation rules\n```\n\n## OpenAPI Spec Generation\n\n```typescript\n\u002F\u002F Use zod-to-openapi or tsoa\nimport { extendZodWithOpenApi } from 'zod-to-openapi'\nimport { z } from 'zod'\n\nextendZodWithOpenApi(z)\n\nconst MarketSchema = z.object({\n  id: z.string().uuid().openapi({ example: 'abc-123' }),\n  name: z.string().openapi({ example: 'Will BTC hit 100k?' }),\n  status: z.enum(['open', 'closed', 'resolved'])\n}).openapi('Market')\n\n\u002F\u002F Auto-generate spec at \u002Fapi\u002Fdocs.json\n```\n\n## Plan-Based Authorization\n\n### Tier-Aware Middleware\n\n```typescript\nenum PlanTier {\n  FREE = 'free',\n  PRO = 'pro',\n  ENTERPRISE = 'enterprise'\n}\n\ninterface PlanLimits {\n  tier: PlanTier\n  rateLimit: number          \u002F\u002F requests per minute\n  maxItems: number           \u002F\u002F max resources\n  features: Set\u003Cstring>      \u002F\u002F enabled features\n}\n\nconst PLAN_LIMITS: Record\u003CPlanTier, PlanLimits> = {\n  [PlanTier.FREE]:       { tier: PlanTier.FREE, rateLimit: 60, maxItems: 100, features: new Set(['read']) },\n  [PlanTier.PRO]:        { tier: PlanTier.PRO, rateLimit: 600, maxItems: 10_000, features: new Set(['read', 'write', 'export']) },\n  [PlanTier.ENTERPRISE]: { tier: PlanTier.ENTERPRISE, rateLimit: 6000, maxItems: Infinity, features: new Set(['read', 'write', 'export', 'audit', 'sso']) },\n}\n\nfunction requireFeature(feature: string) {\n  return (req: Request, res: Response, next: NextFunction) => {\n    const plan = PLAN_LIMITS[req.user.planTier as PlanTier]\n    if (!plan.features.has(feature)) {\n      return apiError(res, 'FORBIDDEN', `Feature \"${feature}\" requires ${PlanTier.PRO} plan or higher`)\n    }\n    next()\n  }\n}\n\nfunction requireQuota(countFn: (userId: string) => Promise\u003Cnumber>) {\n  return async (req: Request, res: Response, next: NextFunction) => {\n    const plan = PLAN_LIMITS[req.user.planTier as PlanTier]\n    const current = await countFn(req.user.id)\n    if (current >= plan.maxItems) {\n      return apiError(res, 'FORBIDDEN', `Plan limit reached (${plan.maxItems} items). Upgrade to increase.`)\n    }\n    next()\n  }\n}\n\n\u002F\u002F Usage\nrouter.post('\u002Fexports', requireFeature('export'), async (req, res) => { \u002F* ... *\u002F })\nrouter.post('\u002Fprojects', requireQuota(countUserProjects), async (req, res) => { \u002F* ... *\u002F })\n```\n\n## Serverless Rate Limiting\n\n### Sliding Window without Redis\n\n```typescript\n\u002F\u002F In-memory sliding window — single-instance only\n\u002F\u002F WARNING: Resets on cold start (serverless). For production multi-instance,\n\u002F\u002F use Redis\u002FUpstash. Add MAX_ENTRIES cap to prevent memory exhaustion from IP flooding.\nconst windows = new Map\u003Cstring, number[]>()\n\nfunction slidingWindowRateLimit(\n  key: string,\n  maxRequests: number,\n  windowMs: number\n): { allowed: boolean; remaining: number; retryAfter: number } {\n  const now = Date.now()\n  const windowStart = now - windowMs\n\n  \u002F\u002F Get or create timestamps array\n  const timestamps = windows.get(key) ?? []\n  const valid = timestamps.filter(t => t > windowStart)\n\n  if (valid.length >= maxRequests) {\n    const oldestInWindow = valid[0]\n    const retryAfter = Math.ceil((oldestInWindow + windowMs - now) \u002F 1000)\n    return { allowed: false, remaining: 0, retryAfter }\n  }\n\n  valid.push(now)\n  windows.set(key, valid)\n  return { allowed: true, remaining: maxRequests - valid.length, retryAfter: 0 }\n}\n\n\u002F\u002F Cleanup stale entries periodically\nsetInterval(() => {\n  const cutoff = Date.now() - 60_000\n  for (const [key, timestamps] of windows) {\n    const valid = timestamps.filter(t => t > cutoff)\n    if (valid.length === 0) windows.delete(key)\n    else windows.set(key, valid)\n  }\n}, 60_000)\n```\n\n## API Key Authentication\n\n```typescript\nimport { randomBytes, createHash, timingSafeEqual } from 'crypto'\n\n\u002F\u002F SECURITY: For in-memory secret comparison, always use timingSafeEqual\n\u002F\u002F DB lookups by hash are safe (constant-time at DB level)\n\n\u002F\u002F Generate: give raw key to user, store hash\nfunction generateApiKey(prefix: string): { raw: string; hash: string } {\n  const raw = `${prefix}_${randomBytes(24).toString('base64url')}`\n  const hash = createHash('sha256').update(raw).digest('hex')\n  return { raw, hash }\n}\n\n\u002F\u002F Verify: hash incoming key, compare with stored hash\nasync function verifyApiKey(rawKey: string): Promise\u003CApiKeyRecord | null> {\n  const hash = createHash('sha256').update(rawKey).digest('hex')\n  return db.apiKey.findFirst({\n    where: { hash, revokedAt: null, expiresAt: { gt: new Date() } }\n  })\n}\n\n\u002F\u002F Middleware\nasync function apiKeyAuth(req: Request, res: Response, next: NextFunction) {\n  const key = req.headers['x-api-key'] as string\n  if (!key) return apiError(res, 'UNAUTHORIZED', 'API key required')\n\n  const record = await verifyApiKey(key)\n  if (!record) return apiError(res, 'UNAUTHORIZED', 'Invalid or expired API key')\n\n  \u002F\u002F Scope check\n  if (!record.scopes.includes(req.method.toLowerCase())) {\n    return apiError(res, 'FORBIDDEN', 'API key lacks required scope')\n  }\n\n  req.user = { id: record.ownerId, keyId: record.id }\n  next()\n}\n```\n\n## Usage Metering and Quota Management\n\n```typescript\ninterface UsageRecord {\n  tenantId: string\n  metric: string      \u002F\u002F 'api_calls' | 'storage_bytes' | 'ai_tokens'\n  value: number\n  period: string      \u002F\u002F '2026-03' (monthly bucket)\n}\n\nasync function trackUsage(tenantId: string, metric: string, increment: number): Promise\u003Cvoid> {\n  const period = new Date().toISOString().slice(0, 7) \u002F\u002F YYYY-MM\n  await db.usage.upsert({\n    where: { tenantId_metric_period: { tenantId, metric, period } },\n    update: { value: { increment } },\n    create: { tenantId, metric, period, value: increment }\n  })\n}\n\nasync function checkQuota(tenantId: string, metric: string, limit: number): Promise\u003Cboolean> {\n  const period = new Date().toISOString().slice(0, 7)\n  const usage = await db.usage.findUnique({\n    where: { tenantId_metric_period: { tenantId, metric, period } }\n  })\n  return (usage?.value ?? 0) \u003C limit\n}\n\n\u002F\u002F Middleware\nfunction meteringMiddleware(metric: string, increment = 1) {\n  return async (req: Request, res: Response, next: NextFunction) => {\n    const plan = PLAN_LIMITS[req.user.planTier as PlanTier]\n    const withinQuota = await checkQuota(req.user.tenantId, metric, plan.rateLimit * 60 * 24)\n    if (!withinQuota) {\n      return apiError(res, 'RATE_LIMITED', `Monthly ${metric} quota exceeded. Upgrade plan.`)\n    }\n    await trackUsage(req.user.tenantId, metric, increment)\n    next()\n  }\n}\n```\n\n**Remember**: Consistent versioning and validation contracts make APIs maintainable across client teams and breaking-change deployments.\n","# API Patterns\n\nREST and GraphQL API design patterns for consistent, versioned, and well-tested interfaces.\n\n## API Versioning\n\n### URL-Based Versioning\n\n```typescript\n\u002F\u002F routes\u002Fv1\u002Fmarkets.ts\n\u002F\u002F routes\u002Fv2\u002Fmarkets.ts\n\u002F\u002F URL: \u002Fapi\u002Fv1\u002Fmarkets, \u002Fapi\u002Fv2\u002Fmarkets\n\n\u002F\u002F Express router setup\nimport { Router } from 'express'\n\nconst v1Router = Router()\nconst v2Router = Router()\n\napp.use('\u002Fapi\u002Fv1', v1Router)\napp.use('\u002Fapi\u002Fv2', v2Router)\n\n\u002F\u002F Deprecation header middleware\nfunction deprecationWarning(version: string, sunsetDate: string) {\n  return (_req: Request, res: Response, next: NextFunction) => {\n    res.setHeader('Deprecation', 'true')\n    res.setHeader('Sunset', sunsetDate)\n    res.setHeader('Link', `\u003C\u002Fapi\u002Fv${parseInt(version) + 1}>; rel=\"successor-version\"`)\n    next()\n  }\n}\n\nv1Router.use(deprecationWarning('1', 'Sat, 01 Jan 2027 00:00:00 GMT'))\n```\n\n### Header-Based Versioning\n\n```typescript\n\u002F\u002F Accept: application\u002Fvnd.api+json;version=2\nfunction versionMiddleware(req: Request, res: Response, next: NextFunction) {\n  const accept = req.headers['accept'] || ''\n  const match = accept.match(\u002Fversion=(\\d+)\u002F)\n  req.apiVersion = match ? parseInt(match[1]) : 1\n  next()\n}\n```\n\n## Schema Validation with Zod\n\n### Request + Response Validation\n\n```typescript\nimport { z } from 'zod'\n\n\u002F\u002F Request schema\nconst CreateMarketSchema = z.object({\n  name: z.string().min(1).max(200),\n  description: z.string().max(2000).optional(),\n  category: z.enum(['sports', 'politics', 'crypto', 'tech']),\n  closeAt: z.string().datetime(),\n  initialLiquidity: z.number().positive().max(1_000_000)\n})\n\n\u002F\u002F Response schema - strip internal fields\nconst MarketResponseSchema = z.object({\n  id: z.string().uuid(),\n  name: z.string(),\n  category: z.string(),\n  status: z.enum(['open', 'closed', 'resolved']),\n  volume: z.number(),\n  createdAt: z.string().datetime()\n})\n\ntype CreateMarketDto = z.infer\u003Ctypeof CreateMarketSchema>\ntype MarketResponse = z.infer\u003Ctypeof MarketResponseSchema>\n\n\u002F\u002F Validation middleware\nfunction validate\u003CT>(schema: z.ZodSchema\u003CT>) {\n  return (req: Request, res: Response, next: NextFunction) => {\n    const result = schema.safeParse(req.body)\n    if (!result.success) {\n      return res.status(400).json({\n        success: false,\n        error: 'Validation failed',\n        code: 'VALIDATION_ERROR',\n        details: result.error.flatten()\n      })\n    }\n    req.validated = result.data\n    next()\n  }\n}\n\n\u002F\u002F Usage\nrouter.post('\u002Fmarkets', validate(CreateMarketSchema), async (req, res) => {\n  const dto = req.validated as CreateMarketDto\n  const market = await marketService.create(dto)\n  const response = MarketResponseSchema.parse(market)\n  res.status(201).json({ success: true, data: response })\n})\n```\n\n## Standardized Error Responses\n\n```typescript\n\u002F\u002F Always: { success, error, code, details? }\ninterface ApiError {\n  success: false\n  error: string\n  code: string\n  details?: unknown\n  requestId?: string\n}\n\ninterface ApiSuccess\u003CT> {\n  success: true\n  data: T\n  meta?: { total?: number; page?: number; limit?: number }\n}\n\nconst ERROR_CODES = {\n  VALIDATION_ERROR: 400,\n  UNAUTHORIZED: 401,\n  FORBIDDEN: 403,\n  NOT_FOUND: 404,\n  CONFLICT: 409,\n  RATE_LIMITED: 429,\n  INTERNAL: 500\n} as const\n\nfunction apiError(\n  res: Response,\n  code: keyof typeof ERROR_CODES,\n  message: string,\n  details?: unknown\n): Response {\n  return res.status(ERROR_CODES[code]).json({\n    success: false,\n    error: message,\n    code,\n    details,\n    requestId: res.locals.requestId\n  } satisfies ApiError)\n}\n```\n\n## Pagination Patterns\n\n### Cursor-Based Pagination (Recommended for large datasets)\n\n```typescript\ninterface CursorPage\u003CT> {\n  items: T[]\n  nextCursor: string | null\n  prevCursor: string | null\n  hasMore: boolean\n}\n\nasync function paginateWithCursor\u003CT extends { id: string; createdAt: Date }>(\n  query: (cursor: string | null, limit: number) => Promise\u003CT[]>,\n  cursor: string | null,\n  limit = 20\n): Promise\u003CCursorPage\u003CT>> {\n  \u002F\u002F Fetch one extra to detect hasMore\n  const items = await query(cursor, limit + 1)\n  const hasMore = items.length > limit\n  const page = hasMore ? items.slice(0, limit) : items\n\n  return {\n    items: page,\n    nextCursor: hasMore ? Buffer.from(page[page.length - 1].id).toString('base64') : null,\n    prevCursor: cursor,\n    hasMore\n  }\n}\n\n\u002F\u002F GET \u002Fapi\u002Fmarkets?cursor=\u003Cbase64>&limit=20\nrouter.get('\u002Fmarkets', async (req, res) => {\n  const cursor = req.query.cursor as string | null\n  const limit = Math.min(parseInt(req.query.limit as string) || 20, 100)\n  const decoded = cursor ? Buffer.from(cursor, 'base64').toString() : null\n\n  const page = await paginateWithCursor(\n    (c, l) => db.market.findMany({\n      take: l,\n      skip: c ? 1 : 0,\n      cursor: c ? { id: c } : undefined,\n      orderBy: { createdAt: 'desc' }\n    }),\n    decoded,\n    limit\n  )\n\n  res.json({ success: true, ...page })\n})\n```\n\n### Offset Pagination (Simple use cases)\n\n```typescript\ninterface OffsetPage\u003CT> {\n  items: T[]\n  total: number\n  page: number\n  limit: number\n  totalPages: number\n}\n\n\u002F\u002F GET \u002Fapi\u002Fmarkets?page=2&limit=20\n```\n\n## Rate Limiting\n\n### Token Bucket with Redis\n\n```typescript\nimport Redis from 'ioredis'\n\nconst redis = new Redis(process.env.REDIS_URL!)\n\nasync function tokenBucket(\n  key: string,\n  capacity: number,\n  refillRate: number   \u002F\u002F tokens per second\n): Promise\u003C{ allowed: boolean; remaining: number; resetIn: number }> {\n  const now = Date.now()\n  const bucketKey = `ratelimit:${key}`\n\n  const script = `\n    local key = KEYS[1]\n    local capacity = tonumber(ARGV[1])\n    local refill_rate = tonumber(ARGV[2])\n    local now = tonumber(ARGV[3])\n\n    local bucket = redis.call('HMGET', key, 'tokens', 'last_refill')\n    local tokens = tonumber(bucket[1]) or capacity\n    local last_refill = tonumber(bucket[2]) or now\n\n    local elapsed = (now - last_refill) \u002F 1000\n    tokens = math.min(capacity, tokens + elapsed * refill_rate)\n\n    if tokens >= 1 then\n      tokens = tokens - 1\n      redis.call('HMSET', key, 'tokens', tokens, 'last_refill', now)\n      redis.call('EXPIRE', key, math.ceil(capacity \u002F refill_rate) + 1)\n      return {1, math.floor(tokens)}\n    else\n      return {0, 0}\n    end\n  `\n\n  const [allowed, remaining] = await redis.eval(\n    script, 1, bucketKey, capacity, refillRate, now\n  ) as [number, number]\n\n  return {\n    allowed: allowed === 1,\n    remaining,\n    resetIn: allowed ? 0 : Math.ceil(1 \u002F refillRate)\n  }\n}\n\nfunction rateLimitMiddleware(capacity: number, refillRate: number) {\n  return async (req: Request, res: Response, next: NextFunction) => {\n    const key = req.user?.id || req.ip || 'anonymous'\n    const result = await tokenBucket(key, capacity, refillRate)\n\n    res.setHeader('X-RateLimit-Limit', capacity)\n    res.setHeader('X-RateLimit-Remaining', result.remaining)\n\n    if (!result.allowed) {\n      res.setHeader('Retry-After', result.resetIn)\n      return apiError(res, 'RATE_LIMITED', 'Too many requests')\n    }\n    next()\n  }\n}\n```\n\n## API Endpoint Testing\n\n```typescript\nimport { describe, it, expect, beforeAll, afterAll } from 'vitest'\nimport supertest from 'supertest'\nimport { app } from '..\u002Fapp'\nimport { db } from '..\u002Fdb'\n\nconst request = supertest(app)\n\ndescribe('POST \u002Fapi\u002Fv1\u002Fmarkets', () => {\n  let authToken: string\n\n  beforeAll(async () => {\n    authToken = await getTestToken()\n  })\n\n  it('creates a market with valid payload', async () => {\n    const payload = {\n      name: 'Will BTC reach 100k?',\n      category: 'crypto',\n      closeAt: new Date(Date.now() + 86400000).toISOString(),\n      initialLiquidity: 1000\n    }\n\n    const res = await request\n      .post('\u002Fapi\u002Fv1\u002Fmarkets')\n      .set('Authorization', `Bearer ${authToken}`)\n      .send(payload)\n      .expect(201)\n\n    expect(res.body.success).toBe(true)\n    expect(res.body.data).toMatchObject({\n      id: expect.any(String),\n      name: payload.name,\n      status: 'open'\n    })\n  })\n\n  it('rejects invalid payload with 400', async () => {\n    const res = await request\n      .post('\u002Fapi\u002Fv1\u002Fmarkets')\n      .set('Authorization', `Bearer ${authToken}`)\n      .send({ name: '' })   \u002F\u002F invalid\n      .expect(400)\n\n    expect(res.body.success).toBe(false)\n    expect(res.body.code).toBe('VALIDATION_ERROR')\n  })\n\n  it('returns 401 without auth token', async () => {\n    const res = await request.post('\u002Fapi\u002Fv1\u002Fmarkets').send({}).expect(401)\n    expect(res.body.code).toBe('UNAUTHORIZED')\n  })\n})\n```\n\n## Breaking Change Detection Checklist\n\n```\nBefore releasing a new API version, verify:\n\nBREAKING changes (require version bump):\n  [ ] Removed a field from response\n  [ ] Changed field type (string → number)\n  [ ] Renamed a field\n  [ ] Changed HTTP method\n  [ ] Removed an endpoint\n  [ ] Changed required → optional (ok) vs optional → required (breaking)\n  [ ] Changed error codes\u002Fformat\n\nNON-BREAKING changes (safe to ship):\n  [ ] Added new optional fields to response\n  [ ] Added new optional query parameters\n  [ ] Added new endpoints\n  [ ] Added new enum values (check client handling)\n  [ ] Relaxed validation rules\n```\n\n## OpenAPI Spec Generation\n\n```typescript\n\u002F\u002F Use zod-to-openapi or tsoa\nimport { extendZodWithOpenApi } from 'zod-to-openapi'\nimport { z } from 'zod'\n\nextendZodWithOpenApi(z)\n\nconst MarketSchema = z.object({\n  id: z.string().uuid().openapi({ example: 'abc-123' }),\n  name: z.string().openapi({ example: 'Will BTC hit 100k?' }),\n  status: z.enum(['open', 'closed', 'resolved'])\n}).openapi('Market')\n\n\u002F\u002F Auto-generate spec at \u002Fapi\u002Fdocs.json\n```\n\n## Plan-Based Authorization\n\n### Tier-Aware Middleware\n\n```typescript\nenum PlanTier {\n  FREE = 'free',\n  PRO = 'pro',\n  ENTERPRISE = 'enterprise'\n}\n\ninterface PlanLimits {\n  tier: PlanTier\n  rateLimit: number          \u002F\u002F requests per minute\n  maxItems: number           \u002F\u002F max resources\n  features: Set\u003Cstring>      \u002F\u002F enabled features\n}\n\nconst PLAN_LIMITS: Record\u003CPlanTier, PlanLimits> = {\n  [PlanTier.FREE]:       { tier: PlanTier.FREE, rateLimit: 60, maxItems: 100, features: new Set(['read']) },\n  [PlanTier.PRO]:        { tier: PlanTier.PRO, rateLimit: 600, maxItems: 10_000, features: new Set(['read', 'write', 'export']) },\n  [PlanTier.ENTERPRISE]: { tier: PlanTier.ENTERPRISE, rateLimit: 6000, maxItems: Infinity, features: new Set(['read', 'write', 'export', 'audit', 'sso']) },\n}\n\nfunction requireFeature(feature: string) {\n  return (req: Request, res: Response, next: NextFunction) => {\n    const plan = PLAN_LIMITS[req.user.planTier as PlanTier]\n    if (!plan.features.has(feature)) {\n      return apiError(res, 'FORBIDDEN', `Feature \"${feature}\" requires ${PlanTier.PRO} plan or higher`)\n    }\n    next()\n  }\n}\n\nfunction requireQuota(countFn: (userId: string) => Promise\u003Cnumber>) {\n  return async (req: Request, res: Response, next: NextFunction) => {\n    const plan = PLAN_LIMITS[req.user.planTier as PlanTier]\n    const current = await countFn(req.user.id)\n    if (current >= plan.maxItems) {\n      return apiError(res, 'FORBIDDEN', `Plan limit reached (${plan.maxItems} items). Upgrade to increase.`)\n    }\n    next()\n  }\n}\n\n\u002F\u002F Usage\nrouter.post('\u002Fexports', requireFeature('export'), async (req, res) => { \u002F* ... *\u002F })\nrouter.post('\u002Fprojects', requireQuota(countUserProjects), async (req, res) => { \u002F* ... *\u002F })\n```\n\n## Serverless Rate Limiting\n\n### Sliding Window without Redis\n\n```typescript\n\u002F\u002F In-memory sliding window — single-instance only\n\u002F\u002F WARNING: Resets on cold start (serverless). For production multi-instance,\n\u002F\u002F use Redis\u002FUpstash. Add MAX_ENTRIES cap to prevent memory exhaustion from IP flooding.\nconst windows = new Map\u003Cstring, number[]>()\n\nfunction slidingWindowRateLimit(\n  key: string,\n  maxRequests: number,\n  windowMs: number\n): { allowed: boolean; remaining: number; retryAfter: number } {\n  const now = Date.now()\n  const windowStart = now - windowMs\n\n  \u002F\u002F Get or create timestamps array\n  const timestamps = windows.get(key) ?? []\n  const valid = timestamps.filter(t => t > windowStart)\n\n  if (valid.length >= maxRequests) {\n    const oldestInWindow = valid[0]\n    const retryAfter = Math.ceil((oldestInWindow + windowMs - now) \u002F 1000)\n    return { allowed: false, remaining: 0, retryAfter }\n  }\n\n  valid.push(now)\n  windows.set(key, valid)\n  return { allowed: true, remaining: maxRequests - valid.length, retryAfter: 0 }\n}\n\n\u002F\u002F Cleanup stale entries periodically\nsetInterval(() => {\n  const cutoff = Date.now() - 60_000\n  for (const [key, timestamps] of windows) {\n    const valid = timestamps.filter(t => t > cutoff)\n    if (valid.length === 0) windows.delete(key)\n    else windows.set(key, valid)\n  }\n}, 60_000)\n```\n\n## API Key Authentication\n\n```typescript\nimport { randomBytes, createHash, timingSafeEqual } from 'crypto'\n\n\u002F\u002F SECURITY: For in-memory secret comparison, always use timingSafeEqual\n\u002F\u002F DB lookups by hash are safe (constant-time at DB level)\n\n\u002F\u002F Generate: give raw key to user, store hash\nfunction generateApiKey(prefix: string): { raw: string; hash: string } {\n  const raw = `${prefix}_${randomBytes(24).toString('base64url')}`\n  const hash = createHash('sha256').update(raw).digest('hex')\n  return { raw, hash }\n}\n\n\u002F\u002F Verify: hash incoming key, compare with stored hash\nasync function verifyApiKey(rawKey: string): Promise\u003CApiKeyRecord | null> {\n  const hash = createHash('sha256').update(rawKey).digest('hex')\n  return db.apiKey.findFirst({\n    where: { hash, revokedAt: null, expiresAt: { gt: new Date() } }\n  })\n}\n\n\u002F\u002F Middleware\nasync function apiKeyAuth(req: Request, res: Response, next: NextFunction) {\n  const key = req.headers['x-api-key'] as string\n  if (!key) return apiError(res, 'UNAUTHORIZED', 'API key required')\n\n  const record = await verifyApiKey(key)\n  if (!record) return apiError(res, 'UNAUTHORIZED', 'Invalid or expired API key')\n\n  \u002F\u002F Scope check\n  if (!record.scopes.includes(req.method.toLowerCase())) {\n    return apiError(res, 'FORBIDDEN', 'API key lacks required scope')\n  }\n\n  req.user = { id: record.ownerId, keyId: record.id }\n  next()\n}\n```\n\n## Usage Metering and Quota Management\n\n```typescript\ninterface UsageRecord {\n  tenantId: string\n  metric: string      \u002F\u002F 'api_calls' | 'storage_bytes' | 'ai_tokens'\n  value: number\n  period: string      \u002F\u002F '2026-03' (monthly bucket)\n}\n\nasync function trackUsage(tenantId: string, metric: string, increment: number): Promise\u003Cvoid> {\n  const period = new Date().toISOString().slice(0, 7) \u002F\u002F YYYY-MM\n  await db.usage.upsert({\n    where: { tenantId_metric_period: { tenantId, metric, period } },\n    update: { value: { increment } },\n    create: { tenantId, metric, period, value: increment }\n  })\n}\n\nasync function checkQuota(tenantId: string, metric: string, limit: number): Promise\u003Cboolean> {\n  const period = new Date().toISOString().slice(0, 7)\n  const usage = await db.usage.findUnique({\n    where: { tenantId_metric_period: { tenantId, metric, period } }\n  })\n  return (usage?.value ?? 0) \u003C limit\n}\n\n\u002F\u002F Middleware\nfunction meteringMiddleware(metric: string, increment = 1) {\n  return async (req: Request, res: Response, next: NextFunction) => {\n    const plan = PLAN_LIMITS[req.user.planTier as PlanTier]\n    const withinQuota = await checkQuota(req.user.tenantId, metric, plan.rateLimit * 60 * 24)\n    if (!withinQuota) {\n      return apiError(res, 'RATE_LIMITED', `Monthly ${metric} quota exceeded. Upgrade plan.`)\n    }\n    await trackUsage(req.user.tenantId, metric, increment)\n    next()\n  }\n}\n```\n\n**Remember**: Consistent versioning and validation contracts make APIs maintainable across client teams and breaking-change deployments.\n",{"name":5,"description":7},"main","70e1c350500ee750987420bbae48837afa906b7e","skills\u002Fapi-patterns","https:\u002F\u002Fgithub.com\u002Frubicanjr\u002FFinCognis","https:\u002F\u002Fgithub.com\u002Frubicanjr","linktr.ee\u002Ffinanskodu","Industrial Engineer & CIO @FinansKodu. Architecting AI-driven financial agents and autonomous workflows. 🏗️ Building systems, not just prompts.","2026-06-09 13:50:15",{"source":8,"skillName":5,"commands":62,"prompt":72,"zipAvailable":73},[63,66,69],{"tool":64,"command":65},"npx","npx skills add rubicanjr\u002FFinCognis --skill api-patterns",{"tool":67,"command":68},"bunx","bunx skills add rubicanjr\u002FFinCognis --skill api-patterns",{"tool":70,"command":71},"pnpm","pnpm dlx skills add rubicanjr\u002FFinCognis --skill api-patterns","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Frubicanjr\u002FFinCognis\u002Fblob\u002Fmain\u002Fskills\u002Fapi-patterns\u002FSKILL.md",true,[],[76,95,104,122,151,170],{"skillNo":77,"name":78,"nameZh":6,"description":79,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":80,"categories":81,"keywords":85,"publishedAt":48},"01M6NWS9DAXJTADT4T5MB49X8W","workflow-router","Goal-based workflow orchestration - routes tasks to specialist agents based on user goals","https:\u002F\u002Fgithub.com\u002Frubicanjr\u002FFinCognis\u002Fblob\u002Fmain\u002Fskills\u002Fworkflow-router\u002FSKILL.md",[82],{"categoryNo":83,"categoryName":84,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000023","Skill与智能体开发",[86,87,88,89,90,91,92,93,94],"workflow router","workflow","router","orchestration","goals","goal","user","workflow orchestration","tasks",{"skillNo":96,"name":97,"nameZh":6,"description":98,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":99,"categories":100,"keywords":101,"publishedAt":48},"01M6NWS3RETSC0SHP16HP66E14","wiring","Wiring Verification","https:\u002F\u002Fgithub.com\u002Frubicanjr\u002FFinCognis\u002Fblob\u002Fmain\u002Fskills\u002Fwiring\u002FSKILL.md",[],[97,102,103],"verification","false",{"skillNo":105,"name":106,"nameZh":6,"description":107,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":108,"categories":109,"keywords":113,"publishedAt":48},"01M6NWS0XW9CVQZBDA1EGW0TWC","websocket-patterns","Connection management, room patterns, reconnection strategies, message buffering, and binary protocol design.","https:\u002F\u002Fgithub.com\u002Frubicanjr\u002FFinCognis\u002Fblob\u002Fmain\u002Fskills\u002Fwebsocket-patterns\u002Fskill.md",[110],{"categoryNo":111,"categoryName":112,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000015","视觉设计",[114,34,115,36,116,117,118,119,120,121],"websocket","websocket patterns","protocol","management","message","strategies","binary","connection",{"skillNo":123,"name":124,"nameZh":6,"description":125,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":126,"categories":127,"keywords":132,"publishedAt":48},"01M6NWRDQDX9ZNETAWRRMQWV52","visual-verdict","Screenshot comparison QA for frontend development. Takes a screenshot of the current implementation, scores it across multiple visual dimensions, and returns a structured PASS\u002FREVISE\u002FFAIL verdict with concrete fixes. Use when implementing UI from a design reference or verifying visual correctness.","https:\u002F\u002Fgithub.com\u002Frubicanjr\u002FFinCognis\u002Fblob\u002Fmain\u002Fskills\u002Fvisual-verdict\u002FSKILL.md",[128,131],{"categoryNo":129,"categoryName":130,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000009","前端开发",{"categoryNo":83,"categoryName":84,"taxonomyType":21,"taxonomyTypeDesc":22},[133,134,135,136,137,138,139,140,141,47,142,143,144,36,145,146,147,148,149,150],"visual","verdict","visual verdict","ui","across","revise","comparison","correctness","implementing","fixes","dimensions","frontend","development","qa","screenshot","returns","use","frontend development",{"skillNo":152,"name":153,"nameZh":6,"description":154,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":155,"categories":156,"keywords":157,"publishedAt":48},"01M6NWR8HSNRS7MNDK2J02QYMN","verification-loop","Comprehensive verification system covering build, types, lint, tests, security, and diff review before a PR.","https:\u002F\u002Fgithub.com\u002Frubicanjr\u002FFinCognis\u002Fblob\u002Fmain\u002Fskills\u002Fverification-loop\u002FSKILL.md",[],[158,159,102,160,161,162,163,164,165,166,167,168,169],"verification loop","loop","lint","review","security","pr","system","diff","build","tests","diff review","types",{"skillNo":171,"name":172,"nameZh":6,"description":173,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":174,"categories":175,"keywords":176,"publishedAt":48},"01M6NWR4HHSFR0VR5EXFRWA747","vector-db-patterns","Embedding strategies, ANN algorithms, hybrid search, RAG chunking strategies, and reranking for semantic search and retrieval.","https:\u002F\u002Fgithub.com\u002Frubicanjr\u002FFinCognis\u002Fblob\u002Fmain\u002Fskills\u002Fvector-db-patterns\u002Fskill.md",[],[177,178,179,180,34,181,182,183,184,119,185,186,187,188,189,47,190,191],"vector db patterns","vector db","db","vector","semantic search","search","embedding strategies","algorithms","chunking","semantic","retrieval","embedding","Hybrid Search","hybrid","rag"]