Vibecosystem api-patterns
API design, versioning, testing, schema validation, and contract testing patterns for REST and GraphQL APIs.
install
source · Clone the upstream repo
git clone https://github.com/vibeeval/vibecosystem
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/vibeeval/vibecosystem "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/api-patterns" ~/.claude/skills/vibeeval-vibecosystem-api-patterns && rm -rf "$T"
manifest:
skills/api-patterns/SKILL.mdsource content
API Patterns
REST and GraphQL API design patterns for consistent, versioned, and well-tested interfaces.
API Versioning
URL-Based Versioning
// routes/v1/markets.ts // routes/v2/markets.ts // URL: /api/v1/markets, /api/v2/markets // Express router setup import { Router } from 'express' const v1Router = Router() const v2Router = Router() app.use('/api/v1', v1Router) app.use('/api/v2', v2Router) // Deprecation header middleware function deprecationWarning(version: string, sunsetDate: string) { return (_req: Request, res: Response, next: NextFunction) => { res.setHeader('Deprecation', 'true') res.setHeader('Sunset', sunsetDate) res.setHeader('Link', `</api/v${parseInt(version) + 1}>; rel="successor-version"`) next() } } v1Router.use(deprecationWarning('1', 'Sat, 01 Jan 2027 00:00:00 GMT'))
Header-Based Versioning
// Accept: application/vnd.api+json;version=2 function versionMiddleware(req: Request, res: Response, next: NextFunction) { const accept = req.headers['accept'] || '' const match = accept.match(/version=(\d+)/) req.apiVersion = match ? parseInt(match[1]) : 1 next() }
Schema Validation with Zod
Request + Response Validation
import { z } from 'zod' // Request schema const CreateMarketSchema = z.object({ name: z.string().min(1).max(200), description: z.string().max(2000).optional(), category: z.enum(['sports', 'politics', 'crypto', 'tech']), closeAt: z.string().datetime(), initialLiquidity: z.number().positive().max(1_000_000) }) // Response schema - strip internal fields const MarketResponseSchema = z.object({ id: z.string().uuid(), name: z.string(), category: z.string(), status: z.enum(['open', 'closed', 'resolved']), volume: z.number(), createdAt: z.string().datetime() }) type CreateMarketDto = z.infer<typeof CreateMarketSchema> type MarketResponse = z.infer<typeof MarketResponseSchema> // Validation middleware function validate<T>(schema: z.ZodSchema<T>) { return (req: Request, res: Response, next: NextFunction) => { const result = schema.safeParse(req.body) if (!result.success) { return res.status(400).json({ success: false, error: 'Validation failed', code: 'VALIDATION_ERROR', details: result.error.flatten() }) } req.validated = result.data next() } } // Usage router.post('/markets', validate(CreateMarketSchema), async (req, res) => { const dto = req.validated as CreateMarketDto const market = await marketService.create(dto) const response = MarketResponseSchema.parse(market) res.status(201).json({ success: true, data: response }) })
Standardized Error Responses
// Always: { success, error, code, details? } interface ApiError { success: false error: string code: string details?: unknown requestId?: string } interface ApiSuccess<T> { success: true data: T meta?: { total?: number; page?: number; limit?: number } } const ERROR_CODES = { VALIDATION_ERROR: 400, UNAUTHORIZED: 401, FORBIDDEN: 403, NOT_FOUND: 404, CONFLICT: 409, RATE_LIMITED: 429, INTERNAL: 500 } as const function apiError( res: Response, code: keyof typeof ERROR_CODES, message: string, details?: unknown ): Response { return res.status(ERROR_CODES[code]).json({ success: false, error: message, code, details, requestId: res.locals.requestId } satisfies ApiError) }
Pagination Patterns
Cursor-Based Pagination (Recommended for large datasets)
interface CursorPage<T> { items: T[] nextCursor: string | null prevCursor: string | null hasMore: boolean } async function paginateWithCursor<T extends { id: string; createdAt: Date }>( query: (cursor: string | null, limit: number) => Promise<T[]>, cursor: string | null, limit = 20 ): Promise<CursorPage<T>> { // Fetch one extra to detect hasMore const items = await query(cursor, limit + 1) const hasMore = items.length > limit const page = hasMore ? items.slice(0, limit) : items return { items: page, nextCursor: hasMore ? Buffer.from(page[page.length - 1].id).toString('base64') : null, prevCursor: cursor, hasMore } } // GET /api/markets?cursor=<base64>&limit=20 router.get('/markets', async (req, res) => { const cursor = req.query.cursor as string | null const limit = Math.min(parseInt(req.query.limit as string) || 20, 100) const decoded = cursor ? Buffer.from(cursor, 'base64').toString() : null const page = await paginateWithCursor( (c, l) => db.market.findMany({ take: l, skip: c ? 1 : 0, cursor: c ? { id: c } : undefined, orderBy: { createdAt: 'desc' } }), decoded, limit ) res.json({ success: true, ...page }) })
Offset Pagination (Simple use cases)
interface OffsetPage<T> { items: T[] total: number page: number limit: number totalPages: number } // GET /api/markets?page=2&limit=20
Rate Limiting
Token Bucket with Redis
import Redis from 'ioredis' const redis = new Redis(process.env.REDIS_URL!) async function tokenBucket( key: string, capacity: number, refillRate: number // tokens per second ): Promise<{ allowed: boolean; remaining: number; resetIn: number }> { const now = Date.now() const bucketKey = `ratelimit:${key}` const script = ` local key = KEYS[1] local capacity = tonumber(ARGV[1]) local refill_rate = tonumber(ARGV[2]) local now = tonumber(ARGV[3]) local bucket = redis.call('HMGET', key, 'tokens', 'last_refill') local tokens = tonumber(bucket[1]) or capacity local last_refill = tonumber(bucket[2]) or now local elapsed = (now - last_refill) / 1000 tokens = math.min(capacity, tokens + elapsed * refill_rate) if tokens >= 1 then tokens = tokens - 1 redis.call('HMSET', key, 'tokens', tokens, 'last_refill', now) redis.call('EXPIRE', key, math.ceil(capacity / refill_rate) + 1) return {1, math.floor(tokens)} else return {0, 0} end ` const [allowed, remaining] = await redis.eval( script, 1, bucketKey, capacity, refillRate, now ) as [number, number] return { allowed: allowed === 1, remaining, resetIn: allowed ? 0 : Math.ceil(1 / refillRate) } } function rateLimitMiddleware(capacity: number, refillRate: number) { return async (req: Request, res: Response, next: NextFunction) => { const key = req.user?.id || req.ip || 'anonymous' const result = await tokenBucket(key, capacity, refillRate) res.setHeader('X-RateLimit-Limit', capacity) res.setHeader('X-RateLimit-Remaining', result.remaining) if (!result.allowed) { res.setHeader('Retry-After', result.resetIn) return apiError(res, 'RATE_LIMITED', 'Too many requests') } next() } }
API Endpoint Testing
import { describe, it, expect, beforeAll, afterAll } from 'vitest' import supertest from 'supertest' import { app } from '../app' import { db } from '../db' const request = supertest(app) describe('POST /api/v1/markets', () => { let authToken: string beforeAll(async () => { authToken = await getTestToken() }) it('creates a market with valid payload', async () => { const payload = { name: 'Will BTC reach 100k?', category: 'crypto', closeAt: new Date(Date.now() + 86400000).toISOString(), initialLiquidity: 1000 } const res = await request .post('/api/v1/markets') .set('Authorization', `Bearer ${authToken}`) .send(payload) .expect(201) expect(res.body.success).toBe(true) expect(res.body.data).toMatchObject({ id: expect.any(String), name: payload.name, status: 'open' }) }) it('rejects invalid payload with 400', async () => { const res = await request .post('/api/v1/markets') .set('Authorization', `Bearer ${authToken}`) .send({ name: '' }) // invalid .expect(400) expect(res.body.success).toBe(false) expect(res.body.code).toBe('VALIDATION_ERROR') }) it('returns 401 without auth token', async () => { const res = await request.post('/api/v1/markets').send({}).expect(401) expect(res.body.code).toBe('UNAUTHORIZED') }) })
Breaking Change Detection Checklist
Before releasing a new API version, verify: BREAKING changes (require version bump): [ ] Removed a field from response [ ] Changed field type (string → number) [ ] Renamed a field [ ] Changed HTTP method [ ] Removed an endpoint [ ] Changed required → optional (ok) vs optional → required (breaking) [ ] Changed error codes/format NON-BREAKING changes (safe to ship): [ ] Added new optional fields to response [ ] Added new optional query parameters [ ] Added new endpoints [ ] Added new enum values (check client handling) [ ] Relaxed validation rules
OpenAPI Spec Generation
// Use zod-to-openapi or tsoa import { extendZodWithOpenApi } from 'zod-to-openapi' import { z } from 'zod' extendZodWithOpenApi(z) const MarketSchema = z.object({ id: z.string().uuid().openapi({ example: 'abc-123' }), name: z.string().openapi({ example: 'Will BTC hit 100k?' }), status: z.enum(['open', 'closed', 'resolved']) }).openapi('Market') // Auto-generate spec at /api/docs.json
Plan-Based Authorization
Tier-Aware Middleware
enum PlanTier { FREE = 'free', PRO = 'pro', ENTERPRISE = 'enterprise' } interface PlanLimits { tier: PlanTier rateLimit: number // requests per minute maxItems: number // max resources features: Set<string> // enabled features } const PLAN_LIMITS: Record<PlanTier, PlanLimits> = { [PlanTier.FREE]: { tier: PlanTier.FREE, rateLimit: 60, maxItems: 100, features: new Set(['read']) }, [PlanTier.PRO]: { tier: PlanTier.PRO, rateLimit: 600, maxItems: 10_000, features: new Set(['read', 'write', 'export']) }, [PlanTier.ENTERPRISE]: { tier: PlanTier.ENTERPRISE, rateLimit: 6000, maxItems: Infinity, features: new Set(['read', 'write', 'export', 'audit', 'sso']) }, } function requireFeature(feature: string) { return (req: Request, res: Response, next: NextFunction) => { const plan = PLAN_LIMITS[req.user.planTier as PlanTier] if (!plan.features.has(feature)) { return apiError(res, 'FORBIDDEN', `Feature "${feature}" requires ${PlanTier.PRO} plan or higher`) } next() } } function requireQuota(countFn: (userId: string) => Promise<number>) { return async (req: Request, res: Response, next: NextFunction) => { const plan = PLAN_LIMITS[req.user.planTier as PlanTier] const current = await countFn(req.user.id) if (current >= plan.maxItems) { return apiError(res, 'FORBIDDEN', `Plan limit reached (${plan.maxItems} items). Upgrade to increase.`) } next() } } // Usage router.post('/exports', requireFeature('export'), async (req, res) => { /* ... */ }) router.post('/projects', requireQuota(countUserProjects), async (req, res) => { /* ... */ })
Serverless Rate Limiting
Sliding Window without Redis
// In-memory sliding window — single-instance only // WARNING: Resets on cold start (serverless). For production multi-instance, // use Redis/Upstash. Add MAX_ENTRIES cap to prevent memory exhaustion from IP flooding. const windows = new Map<string, number[]>() function slidingWindowRateLimit( key: string, maxRequests: number, windowMs: number ): { allowed: boolean; remaining: number; retryAfter: number } { const now = Date.now() const windowStart = now - windowMs // Get or create timestamps array const timestamps = windows.get(key) ?? [] const valid = timestamps.filter(t => t > windowStart) if (valid.length >= maxRequests) { const oldestInWindow = valid[0] const retryAfter = Math.ceil((oldestInWindow + windowMs - now) / 1000) return { allowed: false, remaining: 0, retryAfter } } valid.push(now) windows.set(key, valid) return { allowed: true, remaining: maxRequests - valid.length, retryAfter: 0 } } // Cleanup stale entries periodically setInterval(() => { const cutoff = Date.now() - 60_000 for (const [key, timestamps] of windows) { const valid = timestamps.filter(t => t > cutoff) if (valid.length === 0) windows.delete(key) else windows.set(key, valid) } }, 60_000)
API Key Authentication
import { randomBytes, createHash, timingSafeEqual } from 'crypto' // SECURITY: For in-memory secret comparison, always use timingSafeEqual // DB lookups by hash are safe (constant-time at DB level) // Generate: give raw key to user, store hash function generateApiKey(prefix: string): { raw: string; hash: string } { const raw = `${prefix}_${randomBytes(24).toString('base64url')}` const hash = createHash('sha256').update(raw).digest('hex') return { raw, hash } } // Verify: hash incoming key, compare with stored hash async function verifyApiKey(rawKey: string): Promise<ApiKeyRecord | null> { const hash = createHash('sha256').update(rawKey).digest('hex') return db.apiKey.findFirst({ where: { hash, revokedAt: null, expiresAt: { gt: new Date() } } }) } // Middleware async function apiKeyAuth(req: Request, res: Response, next: NextFunction) { const key = req.headers['x-api-key'] as string if (!key) return apiError(res, 'UNAUTHORIZED', 'API key required') const record = await verifyApiKey(key) if (!record) return apiError(res, 'UNAUTHORIZED', 'Invalid or expired API key') // Scope check if (!record.scopes.includes(req.method.toLowerCase())) { return apiError(res, 'FORBIDDEN', 'API key lacks required scope') } req.user = { id: record.ownerId, keyId: record.id } next() }
Usage Metering and Quota Management
interface UsageRecord { tenantId: string metric: string // 'api_calls' | 'storage_bytes' | 'ai_tokens' value: number period: string // '2026-03' (monthly bucket) } async function trackUsage(tenantId: string, metric: string, increment: number): Promise<void> { const period = new Date().toISOString().slice(0, 7) // YYYY-MM await db.usage.upsert({ where: { tenantId_metric_period: { tenantId, metric, period } }, update: { value: { increment } }, create: { tenantId, metric, period, value: increment } }) } async function checkQuota(tenantId: string, metric: string, limit: number): Promise<boolean> { const period = new Date().toISOString().slice(0, 7) const usage = await db.usage.findUnique({ where: { tenantId_metric_period: { tenantId, metric, period } } }) return (usage?.value ?? 0) < limit } // Middleware function meteringMiddleware(metric: string, increment = 1) { return async (req: Request, res: Response, next: NextFunction) => { const plan = PLAN_LIMITS[req.user.planTier as PlanTier] const withinQuota = await checkQuota(req.user.tenantId, metric, plan.rateLimit * 60 * 24) if (!withinQuota) { return apiError(res, 'RATE_LIMITED', `Monthly ${metric} quota exceeded. Upgrade plan.`) } await trackUsage(req.user.tenantId, metric, increment) next() } }
Remember: Consistent versioning and validation contracts make APIs maintainable across client teams and breaking-change deployments.