AgentSkillsCN

api-design

当用户提出“创建 API 端点”、“构建 REST API”、“添加控制器”、“设计 API”、“实现 CRUD 操作”、“添加校验”、“处理 API 错误”,或任何后端 API 开发相关需求时,可使用此技能。提供 REST API 设计模式、响应格式、校验机制,以及最佳实践建议。

SKILL.md
--- frontmatter
name: api-design
description: Use this skill when the user asks to "create an API endpoint", "build a REST API", "add a controller", "design an API", "implement CRUD operations", "add validation", "handle API errors", or any backend API development work. Provides REST API design patterns, response formats, validation, and best practices.

REST API Design (packages/functions)

For security patterns, see security skill

Directory Structure

code
packages/functions/src/
├── routes/              # Route definitions
│   ├── api.js           # Admin API routes
│   ├── restApiV2.js     # Public REST API v2
│   └── apiHookV1.js     # Webhook routes
├── controllers/         # Request handlers
├── middleware/          # Auth, validation, rate limiting
├── validations/         # Yup schemas
└── helpers/
    └── restApiResponse.js  # Response helpers

Response Format

Response Helpers

javascript
import {
  successResponse,
  errorResponse,
  paginatedResponse,
  itemResponse
} from '../helpers/restApiResponse';

// Single item
ctx.body = itemResponse(customer);

// Paginated list
ctx.body = paginatedResponse(customers, pageInfo, total);

// Error
ctx.status = 400;
ctx.body = errorResponse('Invalid email', 'VALIDATION_ERROR', 400);

Response Structure

TypeFormat
Success{success: true, data, meta, timestamp}
Error{success: false, error: {message, code, statusCode}, timestamp}
Paginated{success: true, data: [], meta: {pagination: {...}}}

HTTP Status Codes

CodeWhen to Use
200Successful GET, PUT
201Successful POST (created)
204Successful DELETE
400Validation error, malformed request
401Missing/invalid authentication
403Authenticated but not authorized
404Resource not found
422Business logic error
429Rate limit exceeded
500Server error

Route Design

RESTful Conventions

ActionMethodRoute
ListGET/resources
Get oneGET/resources/:id
CreatePOST/resources
UpdatePUT/resources/:id
DeleteDELETE/resources/:id
ActionPOST/resources/:id/action

Route Organization

javascript
import Router from 'koa-router';

const router = new Router({prefix: '/api/v2'});

router.use(verifyAuthenticate);
router.use(verifyPlanAccess);

// Resources
router.get('/customers', validateQuery(paginationSchema), getCustomers);
router.get('/customers/:id', getCustomer);
router.post('/customers', validateInput(createSchema), createCustomer);
router.put('/customers/:id', validateInput(updateSchema), updateCustomer);

// Sub-resources
router.get('/customers/:id/rewards', getCustomerRewards);

// Actions
router.post('/customers/:id/points/award', awardPoints);

Input Validation

Yup Schemas

javascript
import * as Yup from 'yup';

export const createCustomerSchema = Yup.object({
  email: Yup.string().email().required(),
  firstName: Yup.string().max(100).optional(),
  points: Yup.number().positive().optional()
});

export const paginationSchema = Yup.object({
  limit: Yup.number().min(1).max(100).default(20),
  cursor: Yup.string().optional()
});

Validation Middleware

javascript
export function validateInput(schema) {
  return async (ctx, next) => {
    try {
      ctx.request.body = await schema.validate(ctx.request.body, {
        stripUnknown: true
      });
      await next();
    } catch (error) {
      ctx.status = 400;
      ctx.body = errorResponse(error.message, 'VALIDATION_ERROR', 400);
    }
  };
}

Controller Pattern

javascript
export async function getOne(ctx) {
  try {
    const {shop} = ctx.state;
    const {id} = ctx.params;

    const resource = await repository.getById(shop.id, id);

    if (!resource) {
      ctx.status = 404;
      ctx.body = errorResponse('Not found', 'NOT_FOUND', 404);
      return;
    }

    ctx.body = itemResponse(pick(resource, publicFields));
  } catch (error) {
    console.error('Error:', error);
    ctx.status = 500;
    ctx.body = errorResponse('Server error', 'INTERNAL_ERROR', 500);
  }
}

Pagination

Cursor-Based (Preferred)

javascript
// Request
GET /api/customers?limit=20&cursor=eyJpZCI6IjEyMyJ9

// Response
{
  "data": [...],
  "meta": {
    "pagination": {
      "hasNext": true,
      "nextCursor": "eyJpZCI6IjE0MyJ9",
      "limit": 20
    }
  }
}

Error Codes

CodeWhen
UNAUTHORIZEDMissing/invalid credentials
FORBIDDENNo permission
PLAN_RESTRICTEDFeature not in plan
VALIDATION_ERRORInvalid input
NOT_FOUNDResource doesn't exist
RATE_LIMITEDToo many requests
INTERNAL_ERRORServer error

Best Practices

DoDon't
Use response helpersReturn raw objects
Set correct status codesReturn 200 for errors
Validate all inputsTrust user input
Pick response fieldsExpose internal fields
Scope queries by shopIdQuery without shop filter
Use cursor paginationUse offset at scale

Checklist

code
□ Uses response helpers (successResponse/errorResponse)
□ Correct HTTP status codes
□ Input validated with Yup schema
□ Queries scoped by shopId
□ Response fields picked (no internal data)
□ Error handling with try-catch
□ Rate limiting applied
□ Authentication middleware