{"slug":"api-design-patterns","title":"api-design-patterns","summary":"REST API design, versioning, error responses, pagination, OpenAPI conventions. Use when designing new API endpoints, reviewing API contracts, or setting up Swagger/OpenAPI documentation.","platform":"Claude","tags":[],"authorName":"LLM Mart","authorSlug":"llm-mart","score":0,"source":"github","price":null,"verified":false,"createdAt":"2026-09-17T16:54:35.02803Z","repo":{"url":"https://github.com/sabahattink/antigravity-fullstack-hq","stars":30,"forks":9,"license":"MIT","updatedAt":"2026-09-21T13:27:15Z"},"bodyHtml":"<hr>\n<h2>name: api-design-patterns\ndescription: REST API design, versioning, error responses, pagination, OpenAPI conventions. Use when designing new API endpoints, reviewing API contracts, or setting up Swagger/OpenAPI documentation.</h2>\n<h1>API Design Patterns</h1>\n<h2>URL Structure</h2>\n<pre><code># Resource naming: plural nouns, lowercase, hyphenated\nGET    /api/v1/users                    # list\nPOST   /api/v1/users                    # create\nGET    /api/v1/users/:id                # read one\nPATCH  /api/v1/users/:id                # partial update\nPUT    /api/v1/users/:id                # full replace\nDELETE /api/v1/users/:id                # delete\n\n# Nested resources (max 2 levels)\nGET    /api/v1/users/:userId/orders\nPOST   /api/v1/users/:userId/orders\nGET    /api/v1/users/:userId/orders/:orderId\n\n# Actions that don't fit CRUD — use verbs as sub-resources\nPOST   /api/v1/users/:id/activate\nPOST   /api/v1/orders/:id/cancel\nPOST   /api/v1/auth/refresh\nPOST   /api/v1/auth/logout\n</code></pre>\n<h2>Standard Response Envelope</h2>\n<pre><code>// types/api-response.ts\nexport interface ApiResponse&lt;T&gt; {\n  success: boolean\n  data:    T | null\n  error:   ApiError | null\n  meta?:   ResponseMeta\n}\n\nexport interface ApiError {\n  code:    string   // machine-readable, stable: 'USER_NOT_FOUND'\n  message: string   // human-readable\n  details?: Record&lt;string, string[]&gt;  // field validation errors\n}\n\nexport interface ResponseMeta {\n  total:  number\n  page:   number\n  limit:  number\n  pages:  number\n}\n\n// Success\n{\n  \"success\": true,\n  \"data\": { \"id\": 1, \"name\": \"Jane\" },\n  \"error\": null\n}\n\n// Error\n{\n  \"success\": false,\n  \"data\": null,\n  \"error\": {\n    \"code\": \"VALIDATION_ERROR\",\n    \"message\": \"Invalid request body\",\n    \"details\": {\n      \"email\": [\"Must be a valid email address\"],\n      \"password\": [\"Must be at least 8 characters\"]\n    }\n  }\n}\n\n// Paginated list\n{\n  \"success\": true,\n  \"data\": [...],\n  \"error\": null,\n  \"meta\": { \"total\": 243, \"page\": 2, \"limit\": 20, \"pages\": 13 }\n}\n</code></pre>\n<h2>NestJS Response Interceptor</h2>\n<pre><code>// common/interceptors/response-transform.interceptor.ts\nimport {\n  Injectable, NestInterceptor, ExecutionContext, CallHandler,\n} from '@nestjs/common'\nimport { Observable, map } from 'rxjs'\nimport { ApiResponse } from '../../types/api-response'\n\n@Injectable()\nexport class ResponseTransformInterceptor&lt;T&gt; implements NestInterceptor&lt;T, ApiResponse&lt;T&gt;&gt; {\n  intercept(context: ExecutionContext, next: CallHandler&lt;T&gt;): Observable&lt;ApiResponse&lt;T&gt;&gt; {\n    return next.handle().pipe(\n      map(data =&gt; ({\n        success: true,\n        data,\n        error: null,\n      }))\n    )\n  }\n}\n\n// Register globally in main.ts\napp.useGlobalInterceptors(new ResponseTransformInterceptor())\n</code></pre>\n<h2>HTTP Status Codes</h2>\n<pre><code>// Use these — don't improvise\nconst STATUS_CODES = {\n  // 2xx Success\n  200: 'OK',                 // GET, PATCH, PUT — returned with data\n  201: 'Created',            // POST — resource created\n  204: 'No Content',         // DELETE, POST actions with no body\n\n  // 3xx Redirect\n  301: 'Moved Permanently',  // URL changed\n  304: 'Not Modified',       // conditional GET, cache valid\n\n  // 4xx Client Error\n  400: 'Bad Request',        // malformed JSON, invalid params\n  401: 'Unauthorized',       // not authenticated\n  403: 'Forbidden',          // authenticated but not authorized\n  404: 'Not Found',          // resource doesn't exist\n  409: 'Conflict',           // duplicate email, version conflict\n  422: 'Unprocessable',      // semantically invalid (business rule)\n  429: 'Too Many Requests',  // rate limited\n\n  // 5xx Server Error\n  500: 'Internal Server Error', // unexpected exception\n  502: 'Bad Gateway',           // upstream service error\n  503: 'Service Unavailable',   // overloaded / maintenance\n}\n</code></pre>\n<h2>Pagination</h2>\n<pre><code>// Query params: consistent naming\n// GET /users?page=2&amp;limit=20&amp;sort=createdAt&amp;order=desc\n\nexport class PaginationQueryDto {\n  @IsOptional() @Type(() =&gt; Number) @IsInt() @Min(1)\n  page: number = 1\n\n  @IsOptional() @Type(() =&gt; Number) @IsInt() @Min(1) @Max(100)\n  limit: number = 20\n\n  @IsOptional() @IsString()\n  sort?: string = 'createdAt'\n\n  @IsOptional() @IsIn(['asc', 'desc'])\n  order?: 'asc' | 'desc' = 'desc'\n\n  @IsOptional() @IsString() @MaxLength(200)\n  search?: string\n}\n\n// Response with cursor-based pagination (for feeds / infinite scroll)\nexport interface CursorPage&lt;T&gt; {\n  data:       T[]\n  nextCursor: string | null  // opaque, base64 encoded\n  hasMore:    boolean\n}\n\n// Encode/decode cursor\nfunction encodeCursor(payload: object): string {\n  return Buffer.from(JSON.stringify(payload)).toString('base64url')\n}\nfunction decodeCursor(cursor: string): unknown {\n  return JSON.parse(Buffer.from(cursor, 'base64url').toString())\n}\n</code></pre>\n<h2>API Versioning</h2>\n<pre><code>// main.ts — URI versioning (recommended for breaking changes)\nimport { VersioningType } from '@nestjs/common'\n\napp.enableVersioning({ type: VersioningType.URI })\n\n// Controller\n@Controller({ path: 'users', version: '1' })\nexport class UsersV1Controller { /* ... */ }\n\n@Controller({ path: 'users', version: '2' })\nexport class UsersV2Controller { /* ... */ }\n\n// Result: GET /v1/users, GET /v2/users\n</code></pre>\n<h2>OpenAPI / Swagger Setup</h2>\n<pre><code>// main.ts\nimport { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'\n\nasync function bootstrap() {\n  const app = await NestFactory.create(AppModule)\n\n  const config = new DocumentBuilder()\n    .setTitle('Example API')\n    .setDescription('Backend API documentation')\n    .setVersion('1.0')\n    .addBearerAuth(\n      { type: 'http', scheme: 'bearer', bearerFormat: 'JWT' },\n      'JWT'\n    )\n    .addServer('http://localhost:3000', 'Development')\n    .addServer('https://api.example.com', 'Production')\n    .build()\n\n  const document = SwaggerModule.createDocument(app, config)\n  SwaggerModule.setup('api/docs', app, document, {\n    swaggerOptions: { persistAuthorization: true },\n  })\n\n  await app.listen(3000)\n}\n</code></pre>\n<pre><code>// Annotate DTOs and controllers\nimport { ApiProperty, ApiPropertyOptional, ApiOperation, ApiResponse } from '@nestjs/swagger'\n\nexport class CreateUserDto {\n  @ApiProperty({ example: 'jane@example.com', description: 'Must be unique' })\n  email: string\n\n  @ApiPropertyOptional({ example: 'admin', enum: UserRole })\n  role?: UserRole\n}\n\n@ApiTags('users')\n@ApiBearerAuth('JWT')\n@Controller('users')\nexport class UsersController {\n  @Post()\n  @ApiOperation({ summary: 'Create a new user' })\n  @ApiResponse({ status: 201, description: 'User created', type: UserResponseDto })\n  @ApiResponse({ status: 409, description: 'Email already in use' })\n  create(@Body() dto: CreateUserDto) { /* ... */ }\n}\n</code></pre>\n<h2>Error Codes Convention</h2>\n<pre><code>// Use namespaced, SCREAMING_SNAKE_CASE error codes\nexport const ErrorCodes = {\n  // Auth\n  AUTH_INVALID_CREDENTIALS: 'AUTH_INVALID_CREDENTIALS',\n  AUTH_TOKEN_EXPIRED:       'AUTH_TOKEN_EXPIRED',\n  AUTH_TOKEN_INVALID:       'AUTH_TOKEN_INVALID',\n  AUTH_INSUFFICIENT_SCOPE:  'AUTH_INSUFFICIENT_SCOPE',\n\n  // Users\n  USER_NOT_FOUND:      'USER_NOT_FOUND',\n  USER_EMAIL_TAKEN:    'USER_EMAIL_TAKEN',\n  USER_DEACTIVATED:    'USER_DEACTIVATED',\n\n  // Validation\n  VALIDATION_ERROR:    'VALIDATION_ERROR',\n  INVALID_UUID:        'INVALID_UUID',\n\n  // Server\n  INTERNAL_ERROR:      'INTERNAL_ERROR',\n  SERVICE_UNAVAILABLE: 'SERVICE_UNAVAILABLE',\n} as const\n</code></pre>\n<h2>Rate Limiting</h2>\n<pre><code>// Install: npm i @nestjs/throttler\n\n// app.module.ts\nThrottlerModule.forRootAsync({\n  inject: [ConfigService],\n  useFactory: (config: ConfigService) =&gt; ({\n    throttlers: [\n      { name: 'short', ttl:  1_000, limit: 3  },   // 3 req/sec\n      { name: 'medium', ttl: 10_000, limit: 20 },   // 20 req/10s\n      { name: 'long', ttl:  60_000, limit: 100 },   // 100 req/min\n    ],\n  }),\n})\n\n// Apply at controller or route level\n@UseGuards(ThrottlerGuard)\n@Throttle({ default: { ttl: 60_000, limit: 5 } })  // 5/min for this endpoint\n@Post('auth/login')\nlogin(@Body() dto: LoginDto) { /* ... */ }\n</code></pre>\n<h2>Request ID Tracing</h2>\n<pre><code>// middleware/request-id.middleware.ts\nimport { Injectable, NestMiddleware } from '@nestjs/common'\nimport { Request, Response, NextFunction } from 'express'\nimport { randomUUID } from 'crypto'\n\n@Injectable()\nexport class RequestIdMiddleware implements NestMiddleware {\n  use(req: Request, res: Response, next: NextFunction) {\n    const requestId = (req.headers['x-request-id'] as string) ?? randomUUID()\n    req.headers['x-request-id'] = requestId\n    res.setHeader('x-request-id', requestId)\n    next()\n  }\n}\n</code></pre>\n<h2>Filtering &amp; Sorting</h2>\n<pre><code>// GET /products?filter[category]=electronics&amp;filter[price][gte]=100&amp;sort=-price,name\n// (minus prefix = descending)\n\nexport class ProductFilterDto {\n  @IsOptional() @IsString()\n  'filter[category]'?: string\n\n  @IsOptional() @Type(() =&gt; Number) @Min(0)\n  'filter[price][gte]'?: number\n\n  @IsOptional() @Type(() =&gt; Number) @Min(0)\n  'filter[price][lte]'?: number\n\n  @IsOptional() @IsString()\n  sort?: string  // comma-separated, minus = desc\n\n  get sortFields(): Array&lt;{ field: string; order: 'ASC' | 'DESC' }&gt; {\n    return (this.sort ?? 'createdAt').split(',').map(s =&gt; ({\n      field: s.replace(/^-/, ''),\n      order: s.startsWith('-') ? 'DESC' : 'ASC',\n    }))\n  }\n}\n</code></pre>\n<h2>Forbidden Patterns</h2>\n<ul>\n<li>Never use verbs in resource URLs (use <code>/orders/:id/cancel</code>, not <code>/cancelOrder</code>)</li>\n<li>Never return different shapes for success vs error — always use the envelope</li>\n<li>Never use <code>200 OK</code> for errors — use the correct 4xx/5xx status</li>\n<li>Never expose database IDs as auto-increment integers in public APIs — use UUIDs</li>\n<li>Never put sensitive data (tokens, passwords, secrets) in query parameters — use headers or body</li>\n<li>Never break versioned API contracts without bumping the version</li>\n<li>Never skip pagination for list endpoints — unbounded queries will OOM in production</li>\n<li>Never return <code>null</code> for missing fields — omit them or use a typed optional</li>\n</ul>\n","files":[{"path":"SKILL.md","sizeBytes":9930,"isText":true}],"reviewScore":null,"reviewSummary":null,"trust":{"provenance":"trusted-source-unreviewed","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow.","bodySource":null},"bodyLocked":false,"purchaseUrl":null,"sourceUrl":null,"report":{"provenance":"trusted-source-unreviewed","screen":{"ran":true,"outcome":"clean","suspicious":0,"notes":0,"hiddenCharacters":false},"virusScan":{"engine":"clamav","status":"clean","scannedAt":"2026-09-17T16:55:10.065752Z","sha256":"C3BFEF7FCC061345256FD4F4C577F12A90B3FCA192409BDDF610DE6F2404D204","sizeBytes":4152},"review":null,"source":{"repositoryUrl":"https://github.com/sabahattink/antigravity-fullstack-hq","path":"skills/api-design-patterns","license":"MIT","commit":"90524b3f8e9ccb8e33e9a0d97e9463d28abe2646","subtreeSha":"D91E4789469E845BE962D56EA90CD506BB365E6CE95A9E1427C094C4C7A9984D","lastSyncedAt":"2026-09-25T23:11:42.035577Z"},"reviewedAt":"2026-09-17T16:57:14.388555Z","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow."},"install":[{"target":"skills-cli","command":"npx skills add https://github.com/sabahattink/antigravity-fullstack-hq/tree/main/skills/api-design-patterns"},{"target":"claude-code","command":"claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install sabahattink-antigravity-fullstack-hq@llmmart"},{"target":"git","command":"git clone https://github.com/sabahattink/antigravity-fullstack-hq.git"}]}