api-design
Use when designing or implementing REST API endpoints with Express. Covers route structure, request validation with Zod, error handling, authentication, pagination, and consistent response formats.
Works with
---
name: api-design
description: Use when designing or implementing REST API endpoints with Express. Covers route structure, request validation with Zod, error handling, authentication, pagination, and consistent response formats.
license: MIT
---
# REST API Design with Express
## Consistent Response Format
Every endpoint must return this shape:
```ts
// Success
{ "success": true, "data": { ... } }
{ "success": true, "data": [...], "meta": { "page": 1, "limit": 20, "total": 87 } }
// Error
{ "success": false, "error": { "code": "VALIDATION_ERROR", "message": "...", "details": [...] } }
```
```ts
// helpers/response.ts
export function success<T>(res: Response, data: T, status = 200) {
return res.status(status).json({ success: true, data });
}
export function paginated<T>(res: Response, data: T[], meta: PaginationMeta) {
return res.status(200).json({ success: true, data, meta });
}
export function error(res: Response, code: string, message: string, status: number, details?: any) {
return res.status(status).json({ success: false, error: { code, message, details } });
}
```
## Route Structure
```
src/
routes/
index.ts # mounts all routers
users.router.ts
projects.router.ts
middleware/
auth.ts
validate.ts
errorHandler.ts
controllers/
users.controller.ts
projects.controller.ts
```
```ts
// routes/index.ts
import { Router } from 'express';
import usersRouter from './users.router';
import projectsRouter from './projects.router';
const router = Router();
router.use('/users', usersRouter);
router.use('/projects', projectsRouter);
export default router;
// routes/users.router.ts
import { Router } from 'express';
import { authenticate, authorize } from '../middleware/auth';
import { validate } from '../middleware/validate';
import { createUserSchema, updateUserSchema } from '../schemas/users';
import * as ctrl from '../controllers/users.controller';
const router = Router();
router.get('/', authenticate, ctrl.listUsers);
router.get('/:id', authenticate, ctrl.getUser);
router.post('/', authenticate, authorize('admin'), validate(createUserSchema), ctrl.createUser);
router.patch('/:id', authenticate, validate(updateUserSchema), ctrl.updateUser);
router.delete('/:id', authenticate, authorize('admin'), ctrl.deleteUser);
export default router;
```
## Request Validation with Zod
```ts
import { z } from 'zod';
import { Request, Response, NextFunction } from 'express';
// Schema definition
export const createUserSchema = z.object({
body: z.object({
email: z.string().email(),
name: z.string().min(1).max(100),
role: z.enum(['user', 'admin']).default('user'),
}),
});
export const listUsersSchema = z.object({
query: z.object({
page: z.coerce.number().int().positive().default(1),
limit: z.coerce.number().int().min(1).max(100).default(20),
search: z.string().optional(),
role: z.enum(['user', 'admin']).optional(),
}),
});
// Validation middleware
export function validate(schema: z.ZodSchema) {
return (req: Request, res: Response, next: NextFunction) => {
const result = schema.safeParse({ body: req.body, query: req.query, params: req.params });
if (!result.success) {
return error(res, 'VALIDATION_ERROR', 'Invalid request', 400, result.error.flatten());
}
req.validated = result.data;
next();
};
}
```
## Error Handling Middleware
```ts
// middleware/errorHandler.ts
import { Request, Response, NextFunction } from 'express';
export class AppError extends Error {
constructor(public statusCode: number, public code: string, message: string) {
super(message);
}
}
export function errorHandler(err: Error, req: Request, res: Response, _next: NextFunction) {
if (err instanceof AppError) {
return res.status(err.statusCode).json({
success: false,
error: { code: err.code, message: err.message },
});
}
console.error('Unhandled error:', err);
return res.status(500).json({
success: false,
error: { code: 'INTERNAL_ERROR', message: 'An unexpected error occurred' },
});
}
// Async wrapper to catch promise rejections
export function asyncHandler(fn: (req: Request, res: Response, next: NextFunction) => Promise<any>) {
return (req: Request, res: Response, next: NextFunction) => fn(req, res, next).catch(next);
}
```
## Authentication Middleware
```ts
import jwt from 'jsonwebtoken';
export function authenticate(req: Request, res: Response, next: NextFunction) {
const header = req.headers.authorization;
if (!header?.startsWith('Bearer ')) {
throw new AppError(401, 'UNAUTHORIZED', 'Missing or invalid token');
}
try {
const payload = jwt.verify(header.slice(7), process.env.JWT_SECRET!) as JwtPayload;
req.user = payload;
next();
} catch {
throw new AppError(401, 'UNAUTHORIZED', 'Token expired or invalid');
}
}
export function authorize(...roles: string[]) {
return (req: Request, res: Response, next: NextFunction) => {
if (!roles.includes(req.user.role)) {
throw new AppError(403, 'FORBIDDEN', 'Insufficient permissions');
}
next();
};
}
```
## Pagination and Filtering
```ts
export async function listUsers(req: Request, res: Response) {
const { page, limit, search, role } = req.validated.query;
const offset = (page - 1) * limit;
let query = db.select().from(users);
if (search) query = query.where(ilike(users.name, `%${search}%`));
if (role) query = query.where(eq(users.role, role));
const [data, [{ count }]] = await Promise.all([
query.limit(limit).offset(offset),
db.select({ count: sql<number>`count(*)` }).from(users),
]);
return paginated(res, data, { page, limit, total: Number(count) });
}
```
## Anti-Patterns
- NEVER return raw database errors to clients. Wrap them in AppError.
- NEVER use `res.send` for API responses. Always use the structured JSON helpers.
- NEVER put business logic in route files. Extract it to controllers or services.
- NEVER trust client input. Validate and sanitize everything with Zod.
- NEVER use sequential IDs in URLs if they expose data enumeration risks. Use UUIDs.More API Design skills
lark-event
larksuite/cli
Lark/Feishu real-time event listening / subscribing / consuming: stream events as NDJSON via `lark-cli event consume <EventKey>` (covers IM messages/reactions/chat changes, Approval status changes, Task updates, VC meeting started/joined/ended, Minutes generated, Whiteboard updated, etc.). Use for Lark bots, real-time message processing, long-running subscribers, streaming webhook/push handlers. Supports `--max-events` / `--timeout` bounded runs and a stderr ready-marker contract — designed for AI agents running as subprocesses.
lark-contact
larksuite/cli
飞书 / Lark 通讯录:按姓名 / 邮箱解析成 open_id,或按 open_id 反查姓名 / 部门 / 邮箱 / 联系方式 / 个人状态 / 签名,以及按关键词搜索当前用户可见的机器人 / 智能体(agent)。当用户提到一个名字要下一步发消息 / 排日程,或拿到 open_id 想查具体信息时使用。不负责部门树遍历、按部门列员工、组织架构图,这类需求走原生 OpenAPI。
lark-openapi-explorer
larksuite/cli
飞书/Lark 原生 OpenAPI 探索:从官方文档库中挖掘未经 CLI 封装的原生 OpenAPI 接口。当用户的需求无法被现有 lark-* skill 或 lark-cli 已注册命令满足,需要查找并调用原生飞书 OpenAPI 时使用。

