Agent Skills: REST API Design (packages/functions)

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.

UncategorizedID: trantuananh-17/product-reviews/api-design

Install this agent skill to your local

pnpm dlx add-skill https://github.com/trantuananh-17/product-reviews/tree/HEAD/.claude/skills/api-design

Skill Files

Browse the full folder contents for api-design.

Download Skill

Loading file tree…

.claude/skills/api-design/SKILL.md

Skill Metadata

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

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

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

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


HTTP Status Codes

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


Route Design

RESTful Conventions

| Action | Method | Route | |--------|--------|-------| | List | GET | /resources | | Get one | GET | /resources/:id | | Create | POST | /resources | | Update | PUT | /resources/:id | | Delete | DELETE | /resources/:id | | Action | POST | /resources/:id/action |

Route Organization

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

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

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

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)

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

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

Error Codes

| Code | When | |------|------| | UNAUTHORIZED | Missing/invalid credentials | | FORBIDDEN | No permission | | PLAN_RESTRICTED | Feature not in plan | | VALIDATION_ERROR | Invalid input | | NOT_FOUND | Resource doesn't exist | | RATE_LIMITED | Too many requests | | INTERNAL_ERROR | Server error |


Best Practices

| Do | Don't | |----|-------| | Use response helpers | Return raw objects | | Set correct status codes | Return 200 for errors | | Validate all inputs | Trust user input | | Pick response fields | Expose internal fields | | Scope queries by shopId | Query without shop filter | | Use cursor pagination | Use offset at scale |


Checklist

□ 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