| name | api-expert |
| version | 2.0.0 |
| description | API architecture and design patterns spanning REST, GraphQL, and gRPC with versioning strategies, rate limiting, gateway configuration, and security best practices. Use when making architectural decisions about API style, designing API gateways, planning API versioning, or establishing cross-cutting API concerns. Do NOT use for specific framework implementation details like FastAPI routes or Apollo resolvers. |
| risk_level | MEDIUM |
| token_budget | 4000 |
API Expert - Code Generation Rules
0. Anti-Hallucination Protocol
0.2 Security Patterns (security rules)
CWE-285: Improper Authorization
- Do not: Rely solely on authentication - always check authorization
- Instead: Verify user has permission for specific resource/action
CWE-918: SSRF
- Do not: Fetch user-provided URLs without validation
- Instead: Allowlist domains, block private IPs, validate schemes
CWE-311: Missing TLS
- Do not: Transmit sensitive data over HTTP
- Instead: HTTPS only, HSTS headers, TLS 1.2+
CWE-770: Missing Rate Limiting
- Do not: Unlimited API requests
- Instead: Rate limiting per user/IP, exponential backoff
CWE-942: Permissive CORS
- Do not:
Access-Control-Allow-Origin: * with credentials
- Instead: Specific origins, validate against allowlist
1. Security Principles
1.1 Authentication & Authorization (CWE-287, CWE-862)
Principle: Always authenticate and authorize. Never trust client-side claims.
app.get('/api/users/:id', (req, res) => {
return getUser(req.params.id);
});
app.get('/api/users/:id', authenticate, async (req, res) => {
const user = await getUser(req.params.id);
if (user.id !== req.user.id && req.user.role !== 'admin') {
throw new ForbiddenError('Not authorized');
}
return user;
});
1.2 Rate Limiting (CWE-770)
Principle: Rate limit all endpoints. Different limits for auth/unauth users.
app.post('/api/login', loginHandler);
import rateLimit from 'express-rate-limit';
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5,
message: { error: 'Too many attempts' },
standardHeaders: true,
legacyHeaders: false,
});
app.post('/api/login', authLimiter, loginHandler);
1.3 Input Validation (CWE-20)
Principle: Validate all input at API boundary. Use schemas, not manual checks.
1.4 Secrets ≠ Code (CWE-798)
Principle: API keys from environment. Never expose in responses or logs.
1.5 CORS Security (CWE-346)
Principle: Strict origin allowlist. Never use * in production.
app.use(cors({ origin: '*' }));
app.use(cors({
origin: ['https://app.example.com', 'https://admin.example.com'],
methods: ['GET', 'POST', 'PUT', 'DELETE'],
credentials: true,
}));
1.6 Error Handling (CWE-209)
Principle: Never expose stack traces or internal details in API errors.
2. Version Requirements
Use these minimum versions:
{
"dependencies": {
"express": "^4.19.0",
"fastify": "^4.26.0",
"zod": "^3.23.0",
"openapi3-ts": "^4.2.0",
"@hono/hono": "^4.1.0"
}
}
3. Code Patterns
3.1 WHEN designing RESTful resources
import { z } from 'zod';
import { Hono } from 'hono';
import { zValidator } from '@hono/zod-validator';
const app = new Hono();
const UserCreateSchema = z.object({
name: z.string().min(1).max(100),
email: z.string().email(),
});
const UserUpdateSchema = UserCreateSchema.partial();
const PaginationSchema = z.object({
page: z.coerce.number().int().min(1).default(1),
limit: z.coerce.number().int().min(1).max(100).default(20),
});
app.get('/api/users', zValidator('query', PaginationSchema), async (c) => {
const { page, limit } = c.req.valid('query');
const offset = (page - 1) * limit;
const [users, total] = await Promise.all([
db.users.findMany({ skip: offset, take: limit }),
db.users.count(),
]);
return c.json({
data: users,
meta: {
page,
limit,
total,
totalPages: Math.ceil(total / limit),
},
});
});
app.post('/api/users', zValidator('json', UserCreateSchema), async (c) => {
const data = c.req.valid('json');
const user = await db.users.create({ data });
return c.json(user, 201);
});
app.get('/api/users/:id', async (c) => {
const id = c.req.param('id');
const user = await db.users.findUnique({ where: { id } });
if (!user) {
return c.json({ error: 'User not found' }, 404);
}
return c.json(user);
});
app.patch('/api/users/:id', zValidator('json', UserUpdateSchema), async (c) => {
const id = c.req.param('id');
const data = c.req.valid('json');
const user = await db.users.update({ where: { id }, data });
return c.json(user);
});
app.delete('/api/users/:id', async (c) => {
const id = c.req.param('id');
await db.users.delete({ where: { id } });
return c.body(null, 204);
});
3.2 WHEN implementing API versioning
app.get('/api/users', getUsersV1);
const v1 = new Hono();
const v2 = new Hono();
v1.get('/users', async (c) => {
const users = await db.users.findMany();
return c.json(users.map(u => ({
id: u.id,
name: u.name,
email: u.email,
})));
});
v2.get('/users', async (c) => {
const users = await db.users.findMany();
return c.json({
data: users.map(u => ({
id: u.id,
attributes: {
name: u.name,
email: u.email,
createdAt: u.createdAt,
},
})),
meta: { version: 'v2' },
});
});
app.route('/api/v1', v1);
app.route('/api/v2', v2);
app.get('/api/users', async (c) => {
const version = c.req.header('API-Version') || 'v1';
switch (version) {
case 'v2':
return getUsersV2(c);
default:
return getUsersV1(c);
}
});
3.3 WHEN implementing OpenAPI documentation
import { OpenAPIHono, createRoute, z } from '@hono/zod-openapi';
const app = new OpenAPIHono();
const UserSchema = z.object({
id: z.string().uuid().openapi({ example: '123e4567-e89b-12d3-a456-426614174000' }),
name: z.string().min(1).openapi({ example: 'John Doe' }),
email: z.string().email().openapi({ example: 'john@example.com' }),
createdAt: z.string().datetime().openapi({ example: '2024-01-15T10:30:00Z' }),
});
const ErrorSchema = z.object({
error: z.string().openapi({ example: 'Resource not found' }),
code: z.string().openapi({ example: 'NOT_FOUND' }),
});
const getUserRoute = createRoute({
method: 'get',
path: '/api/users/{id}',
tags: ['Users'],
summary: 'Get a user by ID',
description: 'Retrieves a single user by their unique identifier',
request: {
params: z.object({
id: z.string().uuid().openapi({ description: 'User ID' }),
}),
},
responses: {
200: {
description: 'User found',
content: {
'application/json': { schema: UserSchema },
},
},
404: {
description: 'User not found',
content: {
'application/json': { schema: ErrorSchema },
},
},
},
security: [{ bearerAuth: [] }],
});
app.openapi(getUserRoute, async (c) => {
const { id } = c.req.valid('param');
const user = await db.users.findUnique({ where: { id } });
if (!user) {
return c.json({ error: 'User not found', code: 'NOT_FOUND' }, 404);
}
return c.json(user);
});
app.doc('/api/openapi.json', {
openapi: '3.1.0',
info: {
title: 'My API',
version: '1.0.0',
description: 'API documentation',
},
servers: [
{ url: 'https://api.example.com', description: 'Production' },
{ url: 'http://localhost:3000', description: 'Development' },
],
security: [{ bearerAuth: [] }],
});
3.4 WHEN implementing error handling
import { HTTPException } from 'hono/http-exception';
import { z } from 'zod';
class ApiError extends Error {
constructor(
public statusCode: number,
public code: string,
message: string,
public details?: unknown
) {
super(message);
}
}
class NotFoundError extends ApiError {
constructor(resource: string) {
super(404, 'NOT_FOUND', `${resource} not found`);
}
}
class ValidationError extends ApiError {
constructor(errors: z.ZodError) {
super(400, 'VALIDATION_ERROR', 'Invalid request data',
errors.issues.map(i => ({
path: i.path.join('.'),
message: i.message,
}))
);
}
}
app.onError((err, c) => {
console.error('API Error:', err);
if (err instanceof z.ZodError) {
return c.json({
error: 'Validation failed',
code: 'VALIDATION_ERROR',
details: err.issues.map(i => ({
path: i.path.join('.'),
message: i.message,
})),
}, 400);
}
if (err instanceof ApiError) {
return c.json({
error: err.message,
code: err.code,
details: err.details,
}, err.statusCode);
}
return c.json({
error: 'Internal server error',
code: 'INTERNAL_ERROR',
}, 500);
});
app.get('/api/users/:id', async (c) => {
const user = await db.users.findUnique({
where: { id: c.req.param('id') }
});
if (!user) {
throw new NotFoundError('User');
}
return c.json(user);
});
3.5 WHEN implementing pagination
import { z } from 'zod';
const CursorPaginationSchema = z.object({
cursor: z.string().optional(),
limit: z.coerce.number().int().min(1).max(100).default(20),
});
app.get('/api/posts', zValidator('query', CursorPaginationSchema), async (c) => {
const { cursor, limit } = c.req.valid('query');
const posts = await db.posts.findMany({
take: limit + 1,
cursor: cursor ? { id: cursor } : undefined,
orderBy: { createdAt: 'desc' },
});
const hasMore = posts.length > limit;
const data = hasMore ? posts.slice(0, -1) : posts;
const nextCursor = hasMore ? data[data.length - 1].id : null;
return c.json({
data,
pagination: {
nextCursor,
hasMore,
},
});
});
const OffsetPaginationSchema = z.object({
page: z.coerce.number().int().min(1).default(1),
limit: z.coerce.number().int().min(1).max(100).default(20),
sortBy: z.enum(['createdAt', 'name', 'updatedAt']).default('createdAt'),
sortOrder: z.enum(['asc', 'desc']).default('desc'),
});
app.get('/api/users', zValidator('query', OffsetPaginationSchema), async (c) => {
const { page, limit, sortBy, sortOrder } = c.req.valid('query');
const skip = (page - 1) * limit;
const [data, total] = await Promise.all([
db.users.findMany({
skip,
take: limit,
orderBy: { [sortBy]: sortOrder },
}),
db.users.count(),
]);
return c.json({
data,
pagination: {
page,
limit,
total,
totalPages: Math.ceil(total / limit),
hasNext: page * limit < total,
hasPrev: page > 1,
},
});
});
3.6 WHEN implementing API authentication
import { jwt, sign, verify } from 'hono/jwt';
import { z } from 'zod';
const JWT_SECRET = process.env.JWT_SECRET!;
const LoginSchema = z.object({
email: z.string().email(),
password: z.string().min(8),
});
app.post('/api/auth/login', zValidator('json', LoginSchema), async (c) => {
const { email, password } = c.req.valid('json');
const user = await db.users.findUnique({ where: { email } });
if (!user || !await verifyPassword(password, user.passwordHash)) {
return c.json({ error: 'Invalid credentials' }, 401);
}
const token = await sign({
sub: user.id,
email: user.email,
role: user.role,
exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24,
}, JWT_SECRET);
return c.json({
token,
expiresIn: 86400,
user: {
id: user.id,
email: user.email,
name: user.name,
},
});
});
const authMiddleware = jwt({ secret: JWT_SECRET });
app.use('/api/protected/*', authMiddleware);
app.get('/api/protected/me', async (c) => {
const payload = c.get('jwtPayload');
const user = await db.users.findUnique({ where: { id: payload.sub } });
return c.json(user);
});
const requireRole = (...roles: string[]) => {
return async (c: Context, next: Next) => {
const payload = c.get('jwtPayload');
if (!roles.includes(payload.role)) {
return c.json({ error: 'Forbidden' }, 403);
}
await next();
};
};
app.get('/api/admin/users', authMiddleware, requireRole('admin'), async (c) => {
const users = await db.users.findMany();
return c.json(users);
});
3.7 WHEN implementing request/response logging
import { logger } from 'hono/logger';
import { timing } from 'hono/timing';
app.use('*', logger());
app.use('*', async (c, next) => {
const requestId = c.req.header('x-request-id') || crypto.randomUUID();
c.set('requestId', requestId);
c.header('X-Request-ID', requestId);
const start = Date.now();
await next();
const duration = Date.now() - start;
console.log(JSON.stringify({
timestamp: new Date().toISOString(),
requestId,
method: c.req.method,
path: c.req.path,
status: c.res.status,
duration,
userAgent: c.req.header('user-agent'),
}));
});
app.use('*', timing());
4. Anti-Patterns
Do not:
- Use verbs in REST URLs (
/getUser, /createUser)
- Return 200 for errors (use proper status codes)
- Expose internal IDs or stack traces in errors
- Allow unlimited pagination (cap at 100)
- Skip input validation on any endpoint
- Use
* for CORS origins in production
- Log sensitive data (passwords, tokens, PII)
5. Testing
ALWAYS write API tests:
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
describe('Users API', () => {
let authToken: string;
beforeAll(async () => {
# ... (additional test cases follow same pattern)
6. Pre-Generation Checklist
Before generating any API code: