Back to skills
SkillHub ClubShip Full StackFull StackBackendDesigner

api-first-design

**API FIRST DESIGN**: 'API 만들어', 'API 설계', '엔드포인트', 'REST', 'Swagger', 'OpenAPI', 'DTO', 'CRUD' 요청 시 자동 발동. *.controller.ts/*.dto.ts/routes/** 파일 작업 시 자동 적용. Contract-First, 표준 응답 포맷, 타입 자동 생성.

Packaged view

This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.

Stars
0
Hot score
74
Updated
March 20, 2026
Overall rating
C0.8
Composite score
0.8
Best-practice grade
C62.8

Install command

npx @skill-hub/cli install monicajeon28-gmcruise-api-first-design
api-designopenapirestbackendtypescript

Repository

monicajeon28/GMcruise

Skill path: .claude/skills/api-first-design

**API FIRST DESIGN**: 'API 만들어', 'API 설계', '엔드포인트', 'REST', 'Swagger', 'OpenAPI', 'DTO', 'CRUD' 요청 시 자동 발동. *.controller.ts/*.dto.ts/routes/** 파일 작업 시 자동 적용. Contract-First, 표준 응답 포맷, 타입 자동 생성.

Open repository

Best for

Primary workflow: Ship Full Stack.

Technical facets: Full Stack, Backend, Designer.

Target audience: everyone.

License: name: MIT.

Original source

Catalog source: SkillHub Club.

Repository owner: monicajeon28.

This is still a mirrored public skill entry. Review the repository before installing into production workflows.

What it helps with

  • Install api-first-design into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
  • Review https://github.com/monicajeon28/GMcruise before adding api-first-design to shared team environments
  • Use api-first-design for development workflows

Works across

Claude CodeCodex CLIGemini CLIOpenCode

Favorites: 0.

Sub-skills: 0.

Aggregator: No.

Original source / Raw SKILL.md

---
name: api-first-design
description: "**API FIRST DESIGN**: 'API 만들어', 'API 설계', '엔드포인트', 'REST', 'Swagger', 'OpenAPI', 'DTO', 'CRUD' 요청 시 자동 발동. *.controller.ts/*.dto.ts/routes/** 파일 작업 시 자동 적용. Contract-First, 표준 응답 포맷, 타입 자동 생성."
allowed-tools:
  - Read
  - Glob
  - Grep
---

# API First Design Skill

> **Version**: 1.0.0
> **Purpose**: Swagger/OpenAPI 기반 Contract-First API 설계 및 일관된 응답 포맷 보장
> **Target**: NestJS + Next.js + Expo Monorepo

---

## Document Loading Strategy

**전체 문서를 로드하지 않습니다!** 상황에 따라 필요한 문서만 로드:

```yaml
Document_Loading_Strategy:
  Step_1_Detect_Framework:
    - 파일 확장자, import 문, 데코레이터로 프레임워크 감지
    - 불명확하면 package.json/requirements.txt 확인

  Step_2_Load_Required_Docs:
    Universal_Always_Load:
      - "core/contract-first.md"        # Contract-First 원칙 (항상)
      - "core/response-format.md"       # 표준 응답 포맷 (항상)
      - "core/error-codes.md"           # 에러 코드 체계 (항상)

    Framework_Specific_Load:
      NestJS: "frameworks/nestjs-swagger.md"     # NestJS Swagger 데코레이터
      Express: "frameworks/express-openapi.md"   # Express OpenAPI
      FastAPI: "frameworks/fastapi-openapi.md"   # Python FastAPI
      Django: "frameworks/django-rest.md"        # Django REST Framework
      SpringBoot: "frameworks/spring-openapi.md" # Spring OpenAPI
      Go: "frameworks/go-swagger.md"             # Go Swagger
      Rails: "frameworks/rails-openapi.md"       # Rails OpenAPI

    Type_Generation_Load:
      TypeScript: "patterns/type-generation.md"  # swagger-typescript-api
      Python: "patterns/python-types.md"         # pydantic
      Go: "patterns/go-types.md"                 # oapi-codegen

    Context_Specific_Load:
      Controller_Template: "templates/controller-template.md"
      OpenAPI_YAML: "templates/openapi-yaml.md"
      Checklist: "quick-reference/checklist.md"
      Anti_Patterns: "quick-reference/anti-patterns.md"
```

---

## Auto Trigger Conditions

```yaml
Auto_Trigger_Conditions:
  File_Patterns:
    - "*.controller.ts, *.controller.js"
    - "*.dto.ts, *.dto.js"
    - "**/routes/**, **/api/**"
    - "*.router.ts, *.router.js"
    - "openapi.yaml, swagger.yaml"

  Keywords_KO:
    - "API 만들어, API 작성, API 설계"
    - "엔드포인트, 라우트, 라우터"
    - "요청, 응답, 리퀘스트, 리스폰스"
    - "REST, RESTful, GraphQL"
    - "Swagger, OpenAPI, API 문서"
    - "DTO, 데이터 전송, 스키마"
    - "CRUD, 생성, 조회, 수정, 삭제"

  Keywords_EN:
    - "API, endpoint, route, router"
    - "request, response, REST, RESTful"
    - "Swagger, OpenAPI, API docs"
    - "DTO, schema, contract"
    - "CRUD, create, read, update, delete"

  Code_Patterns:
    - "@Controller, @Get, @Post, @Put, @Delete"
    - "@ApiOperation, @ApiResponse, @ApiTags"
    - "router.get, router.post, app.get"
    - "@app.get, @app.post"  # FastAPI
```

---

## Base Knowledge (다른 스킬 참조)

> **에러 처리 기본**: `clean-code-mastery/core/principles.md` 참조
> **인증/보안 패턴**: `security-shield` 참조 (JWT, 세션, CSRF)
> **입력 검증 기본**: `clean-code-mastery/core/security.md` 참조

---

## Core Concepts

### 1. Contract-First Workflow
```
OpenAPI Spec 정의 → 스펙 리뷰 → Type 자동 생성 → Backend/Frontend 구현
```

### 2. Standard Response Format
```typescript
// 성공
{ success: true, data: T, meta?: ApiMeta, timestamp: string }

// 에러
{ success: false, error: ApiError, timestamp: string }
```

### 3. Error Code System
```
패턴: DOMAIN_ACTION_REASON
예시: AUTH_TOKEN_EXPIRED, USER_NOT_FOUND, VALIDATION_FAILED
```

---

## Quick Reference

```yaml
필수_데코레이터:
  Controller:
    - "@ApiTags('Resources')"      # 복수형
    - "@ApiBearerAuth('access-token')"

  Method:
    - "@ApiOperation({ summary, description })"
    - "@ApiResponse({ status, description, type })"
    - "@ApiParam / @ApiQuery / @ApiBody"

  DTO:
    - "@ApiProperty({ description, example, format })"
    - "@ApiPropertyOptional()"

버전_전략:
  형식: "/api/v1/resources"
  지원: "현재 버전 + 이전 버전(12개월)"
```

---

## Module Files

| File | Description |
|------|-------------|
| `core/contract-first.md` | Contract-First 워크플로우, 디렉토리 구조 |
| `core/response-format.md` | 표준 응답 포맷, ApiResponse/ApiError |
| `core/error-codes.md` | 에러 코드 체계, HTTP 상태 매핑 |
| `patterns/swagger-decorators.md` | NestJS Swagger 데코레이터 가이드 |
| `patterns/type-generation.md` | 타입 자동 생성, React Query 통합 |
| `templates/controller-template.md` | 완전한 Controller 템플릿 |
| `templates/openapi-yaml.md` | OpenAPI YAML 템플릿 |
| `quick-reference/checklist.md` | API 개발/배포 체크리스트 |
| `quick-reference/anti-patterns.md` | API 안티패턴 목록 |

---

**Document Version**: 1.0.0
**Last Updated**: 2025-12-09

---

## Code Generation Tools (2025 권장)

### 🏆 1위: Orval (강력 추천)

```yaml
Orval:
  설명: "OpenAPI → TypeScript 클라이언트 + React Query 훅 자동 생성"
  주간_다운로드: "513K+"
  GitHub_Stars: "4,700+"
  
  핵심_장점:
    - React Query/TanStack Query 훅 자동 생성
    - MSW(Mock Service Worker) 핸들러 자동 생성
    - Zod 스키마 검증 통합 가능
    - TypeScript 특화, 타입 안전
    - 설정 간단 (orval.config.ts)
    
  지원_클라이언트:
    - Axios (기본)
    - Fetch API
    - React Query (TanStack Query)
    - SWR
    - Vue Query
    - Svelte Query
    - Angular HttpClient

설치_및_설정:
  install: |
    npm install orval -D
    
  config_file: |
    // orval.config.ts
    import { defineConfig } from 'orval';
    
    export default defineConfig({
      api: {
        input: './openapi.yaml',  // OpenAPI 스펙 경로
        output: {
          target: './src/api/generated.ts',
          client: 'react-query',  // React Query 훅 생성
          mode: 'tags-split',     // 태그별 파일 분리
          mock: true,             // MSW mock 자동 생성
        },
      },
    });
    
  실행: |
    npx orval
    # 또는 package.json scripts에 추가
    "scripts": {
      "api:generate": "orval"
    }

생성_결과_예시:
  input_openapi: |
    paths:
      /users/{id}:
        get:
          operationId: getUser
          parameters:
            - name: id
              in: path
          responses:
            200:
              content:
                application/json:
                  schema:
                    $ref: '#/components/schemas/User'
                    
  output_react_query: |
    // 자동 생성된 코드
    export const useGetUser = (id: string) => {
      return useQuery({
        queryKey: ['getUser', id],
        queryFn: () => axios.get<User>(`/users/${id}`),
      });
    };
    
  output_msw_mock: |
    // 자동 생성된 MSW 핸들러
    export const getGetUserMock = () => 
      http.get('/users/:id', () => {
        return HttpResponse.json(getUserResponseMock());
      });
```

### 2위: openapi-typescript

```yaml
openapi-typescript:
  설명: "OpenAPI → TypeScript 타입만 생성 (가벼움)"
  주간_다운로드: "1.7M+"
  
  적합한_경우:
    - 타입만 필요하고 클라이언트는 직접 작성할 때
    - 번들 크기 최소화가 중요할 때
    
  사용: |
    npx openapi-typescript ./openapi.yaml -o ./src/api/types.ts
```

### ❌ 피해야 할 도구

```yaml
Legacy_Tools:
  swagger-codegen:
    문제: "레거시, 업데이트 느림, 현대 프레임워크 미지원"
    대안: "Orval 사용"
    
  OpenAPI_Generator_CLI:
    문제: "설정 복잡, Java 의존성, 느림"
    대안: "Orval 또는 openapi-typescript"
    
  swagger-typescript-api:
    문제: "React Query 지원 약함, mock 생성 없음"
    대안: "Orval"
```

### Contract-First + Orval 워크플로우

```yaml
Recommended_Workflow:
  1_API_설계:
    - OpenAPI YAML/JSON 작성
    - API 스펙 리뷰 (팀)
    
  2_코드_생성:
    - "npx orval" 실행
    - TypeScript 타입 생성
    - React Query 훅 생성
    - MSW mock 생성
    
  3_개발:
    - 프론트엔드: 생성된 훅 사용
    - 백엔드: 스펙에 맞춰 구현
    - 테스트: MSW mock으로 독립 테스트
    
  4_API_변경_시:
    - OpenAPI 스펙 수정
    - "npx orval" 재실행
    - TypeScript 컴파일 에러로 변경점 파악
```

### package.json 권장 스크립트

```json
{
  "scripts": {
    "api:generate": "orval",
    "api:watch": "orval --watch",
    "api:validate": "swagger-cli validate ./openapi.yaml"
  }
}
```

---

**Updated**: 2025-12-10
**Added**: Orval 코드 생성 도구 권장 (우수사례 피드백)


---

## Referenced Files

> The following files are referenced in this skill and included for context.

### core/contract-first.md

```markdown
# Contract-First Workflow

## 개념

**Contract-First**: API 구현 전에 OpenAPI 스펙을 먼저 정의하고, 이를 기반으로 코드 생성

```
┌─────────────────────────────────────────────────────────────────┐
│                    Contract-First Workflow                       │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│   1. OpenAPI Spec 정의 (YAML/JSON)                              │
│          ↓                                                      │
│   2. 스펙 리뷰 (Frontend + Backend 합의)                        │
│          ↓                                                      │
│   3. Type 자동 생성 (swagger-typescript-api)                    │
│          ↓                                                      │
│   4. Backend: NestJS Controller/DTO 구현                        │
│          ↓                                                      │
│   5. Frontend: 생성된 타입으로 API Client 구현                  │
│          ↓                                                      │
│   6. Swagger UI로 API 문서화 자동 완성                          │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘
```

## 장점

| 장점 | 설명 |
|------|------|
| **동기화** | Frontend/Backend 간 타입 불일치 제거 |
| **병렬 개발** | 스펙 합의 후 FE/BE 동시 개발 가능 |
| **문서화** | 코드와 문서가 항상 일치 |
| **테스트** | 스펙 기반 자동 테스트 생성 가능 |
| **타입 안전성** | End-to-End 타입 보장 |

## 디렉토리 구조

```
packages/
└── shared/
    └── api/
        ├── openapi/
        │   ├── openapi.yaml          # 메인 OpenAPI 스펙
        │   ├── paths/                # 경로별 스펙 분할
        │   │   ├── auth.yaml
        │   │   ├── users.yaml
        │   │   └── products.yaml
        │   ├── components/           # 재사용 컴포넌트
        │   │   ├── schemas.yaml
        │   │   ├── responses.yaml
        │   │   ├── parameters.yaml
        │   │   └── security.yaml
        │   └── examples/             # 요청/응답 예시
        │       └── user-examples.yaml
        ├── generated/                # 자동 생성된 타입
        │   ├── api.ts                # API Client
        │   ├── types.ts              # TypeScript 타입
        │   └── index.ts
        └── scripts/
            └── generate-types.ts     # 타입 생성 스크립트
```

## API Versioning

### URL Path Versioning (권장)

```typescript
// /api/v1/users
// /api/v2/users

// main.ts
app.setGlobalPrefix('api/v1');

// 또는 Controller 레벨에서
@Controller({ path: 'users', version: '1' })
export class UsersControllerV1 {}

@Controller({ path: 'users', version: '2' })
export class UsersControllerV2 {}
```

### Versioning 설정

```typescript
// main.ts
import { VersioningType } from '@nestjs/common';

app.enableVersioning({
  type: VersioningType.URI,
  defaultVersion: '1',
  prefix: 'api/v',
});
```

### 버전 전환 가이드라인

```yaml
Breaking_Changes:
  - 필드 삭제 또는 이름 변경
  - 필수 필드 추가
  - 응답 구조 변경
  - 인증 방식 변경
  - 에러 코드 체계 변경

Non_Breaking_Changes:
  - 선택적 필드 추가
  - 새 엔드포인트 추가
  - 에러 메시지 개선
  - 성능 개선

Version_Support:
  - 현재 버전 (v2): Full support
  - 이전 버전 (v1): Maintenance (12개월)
  - 그 이전: Deprecated (6개월 후 제거)

Deprecation_Headers:
  Deprecation: "true"
  Sunset: "2025-06-01T00:00:00Z"
  Link: "<https://api.example.com/v2/users>; rel=\"successor-version\""
```

```

### core/response-format.md

```markdown
# Standard Response Format

## 성공 응답 (Success Response)

```typescript
// packages/shared/api/types/response.ts

/**
 * 표준 성공 응답 포맷
 * 모든 API는 이 포맷을 따라야 함
 */
export interface ApiResponse<T> {
  /** 성공 여부 */
  success: true;
  /** 응답 데이터 */
  data: T;
  /** 메타 정보 (페이지네이션 등) */
  meta?: ApiMeta;
  /** 응답 시간 (ISO 8601) */
  timestamp: string;
}

export interface ApiMeta {
  /** 현재 페이지 (1부터 시작) */
  page?: number;
  /** 페이지당 항목 수 */
  limit?: number;
  /** 전체 항목 수 */
  total?: number;
  /** 전체 페이지 수 */
  totalPages?: number;
  /** 다음 페이지 존재 여부 */
  hasNext?: boolean;
  /** 이전 페이지 존재 여부 */
  hasPrev?: boolean;
}

// 사용 예시
type UserResponse = ApiResponse<User>;
type UsersListResponse = ApiResponse<User[]>;
```

## 에러 응답 (Error Response)

```typescript
/**
 * 표준 에러 응답 포맷
 * 모든 에러는 이 포맷을 따라야 함
 */
export interface ApiErrorResponse {
  /** 성공 여부 */
  success: false;
  /** 에러 정보 */
  error: ApiError;
  /** 응답 시간 (ISO 8601) */
  timestamp: string;
}

export interface ApiError {
  /** HTTP 상태 코드 */
  statusCode: number;
  /** 에러 코드 (앱 내부 코드) */
  code: string;
  /** 사용자에게 표시할 메시지 */
  message: string;
  /** 개발자용 상세 메시지 (production에서 숨김) */
  details?: string;
  /** 필드별 검증 에러 */
  errors?: FieldError[];
  /** 에러 추적 ID (로그 조회용) */
  traceId?: string;
}

export interface FieldError {
  /** 필드명 (e.g., "email", "user.address.zipCode") */
  field: string;
  /** 에러 메시지 */
  message: string;
  /** 검증 실패한 값 (민감 정보 제외) */
  value?: unknown;
  /** 검증 규칙 (e.g., "isEmail", "minLength") */
  rule?: string;
}
```

## Response DTO for Swagger

```typescript
// Wrapped Response DTO (Standard Format)

@ApiExtraModels(UserDto)
export class UserResponseDto {
  @ApiProperty({ example: true })
  success!: true;

  @ApiProperty({ type: UserDto })
  data!: UserDto;

  @ApiProperty({ example: '2024-01-15T09:30:00.000Z' })
  timestamp!: string;
}

@ApiExtraModels(UserDto)
export class UsersListResponseDto {
  @ApiProperty({ example: true })
  success!: true;

  @ApiProperty({ type: [UserDto] })
  data!: UserDto[];

  @ApiProperty({ type: ApiMetaDto })
  meta!: ApiMetaDto;

  @ApiProperty({ example: '2024-01-15T09:30:00.000Z' })
  timestamp!: string;
}

export class ApiMetaDto {
  @ApiProperty({ example: 1, description: '현재 페이지' })
  page!: number;

  @ApiProperty({ example: 20, description: '페이지당 항목 수' })
  limit!: number;

  @ApiProperty({ example: 100, description: '전체 항목 수' })
  total!: number;

  @ApiProperty({ example: 5, description: '전체 페이지 수' })
  totalPages!: number;

  @ApiProperty({ example: true, description: '다음 페이지 존재 여부' })
  hasNext!: boolean;

  @ApiProperty({ example: false, description: '이전 페이지 존재 여부' })
  hasPrev!: boolean;
}
```

## Generic Response Helper

```typescript
// Generic 타입을 Swagger에서 처리하기 위한 헬퍼

import { applyDecorators, Type } from '@nestjs/common';
import { ApiOkResponse, getSchemaPath, ApiExtraModels } from '@nestjs/swagger';

/**
 * 표준 성공 응답 데코레이터
 */
export const ApiStandardResponse = <TModel extends Type<any>>(
  model: TModel,
  isArray = false,
) => {
  return applyDecorators(
    ApiExtraModels(model),
    ApiOkResponse({
      schema: {
        allOf: [
          {
            properties: {
              success: { type: 'boolean', example: true },
              timestamp: { type: 'string', example: '2024-01-15T09:30:00.000Z' },
            },
          },
          {
            properties: {
              data: isArray
                ? { type: 'array', items: { $ref: getSchemaPath(model) } }
                : { $ref: getSchemaPath(model) },
            },
          },
        ],
      },
    }),
  );
};

/**
 * 페이지네이션 응답 데코레이터
 */
export const ApiPaginatedResponse = <TModel extends Type<any>>(model: TModel) => {
  return applyDecorators(
    ApiExtraModels(model),
    ApiOkResponse({
      schema: {
        allOf: [
          {
            properties: {
              success: { type: 'boolean', example: true },
              timestamp: { type: 'string', example: '2024-01-15T09:30:00.000Z' },
            },
          },
          {
            properties: {
              data: {
                type: 'array',
                items: { $ref: getSchemaPath(model) },
              },
              meta: {
                type: 'object',
                properties: {
                  page: { type: 'number', example: 1 },
                  limit: { type: 'number', example: 20 },
                  total: { type: 'number', example: 100 },
                  totalPages: { type: 'number', example: 5 },
                  hasNext: { type: 'boolean', example: true },
                  hasPrev: { type: 'boolean', example: false },
                },
              },
            },
          },
        ],
      },
    }),
  );
};

// 사용 예시
@Controller('users')
export class UsersController {
  @Get(':id')
  @ApiStandardResponse(UserDto)  // 단일 객체
  getUser(@Param('id') id: string) {}

  @Get()
  @ApiPaginatedResponse(UserDto)  // 배열 + 페이지네이션
  getUsers(@Query() query: GetUsersQueryDto) {}
}
```

```

### core/error-codes.md

```markdown
# Error Codes System

## 에러 코드 체계

```typescript
/**
 * 애플리케이션 에러 코드
 * 형식: DOMAIN_ACTION_REASON
 */
export const ErrorCodes = {
  // Authentication (AUTH_*)
  AUTH_INVALID_CREDENTIALS: 'AUTH_INVALID_CREDENTIALS',
  AUTH_TOKEN_EXPIRED: 'AUTH_TOKEN_EXPIRED',
  AUTH_TOKEN_INVALID: 'AUTH_TOKEN_INVALID',
  AUTH_REFRESH_FAILED: 'AUTH_REFRESH_FAILED',
  AUTH_UNAUTHORIZED: 'AUTH_UNAUTHORIZED',
  AUTH_FORBIDDEN: 'AUTH_FORBIDDEN',

  // User (USER_*)
  USER_NOT_FOUND: 'USER_NOT_FOUND',
  USER_ALREADY_EXISTS: 'USER_ALREADY_EXISTS',
  USER_EMAIL_TAKEN: 'USER_EMAIL_TAKEN',
  USER_INVALID_PASSWORD: 'USER_INVALID_PASSWORD',
  USER_DEACTIVATED: 'USER_DEACTIVATED',

  // Validation (VALIDATION_*)
  VALIDATION_FAILED: 'VALIDATION_FAILED',
  VALIDATION_MISSING_FIELD: 'VALIDATION_MISSING_FIELD',
  VALIDATION_INVALID_FORMAT: 'VALIDATION_INVALID_FORMAT',

  // Resource (RESOURCE_*)
  RESOURCE_NOT_FOUND: 'RESOURCE_NOT_FOUND',
  RESOURCE_ALREADY_EXISTS: 'RESOURCE_ALREADY_EXISTS',
  RESOURCE_CONFLICT: 'RESOURCE_CONFLICT',

  // Server (SERVER_*)
  SERVER_INTERNAL_ERROR: 'SERVER_INTERNAL_ERROR',
  SERVER_SERVICE_UNAVAILABLE: 'SERVER_SERVICE_UNAVAILABLE',
  SERVER_TIMEOUT: 'SERVER_TIMEOUT',

  // Rate Limit (RATE_*)
  RATE_LIMIT_EXCEEDED: 'RATE_LIMIT_EXCEEDED',
} as const;

export type ErrorCode = typeof ErrorCodes[keyof typeof ErrorCodes];
```

## HTTP 상태 코드 매핑

```typescript
/**
 * 에러 코드 → HTTP 상태 코드 매핑
 */
export const ErrorCodeToStatus: Record<ErrorCode, number> = {
  // 400 Bad Request
  [ErrorCodes.VALIDATION_FAILED]: 400,
  [ErrorCodes.VALIDATION_MISSING_FIELD]: 400,
  [ErrorCodes.VALIDATION_INVALID_FORMAT]: 400,

  // 401 Unauthorized
  [ErrorCodes.AUTH_INVALID_CREDENTIALS]: 401,
  [ErrorCodes.AUTH_TOKEN_EXPIRED]: 401,
  [ErrorCodes.AUTH_TOKEN_INVALID]: 401,
  [ErrorCodes.AUTH_UNAUTHORIZED]: 401,

  // 403 Forbidden
  [ErrorCodes.AUTH_FORBIDDEN]: 403,
  [ErrorCodes.USER_DEACTIVATED]: 403,

  // 404 Not Found
  [ErrorCodes.USER_NOT_FOUND]: 404,
  [ErrorCodes.RESOURCE_NOT_FOUND]: 404,

  // 409 Conflict
  [ErrorCodes.USER_ALREADY_EXISTS]: 409,
  [ErrorCodes.USER_EMAIL_TAKEN]: 409,
  [ErrorCodes.RESOURCE_ALREADY_EXISTS]: 409,
  [ErrorCodes.RESOURCE_CONFLICT]: 409,

  // 429 Too Many Requests
  [ErrorCodes.RATE_LIMIT_EXCEEDED]: 429,

  // 500 Internal Server Error
  [ErrorCodes.SERVER_INTERNAL_ERROR]: 500,

  // 503 Service Unavailable
  [ErrorCodes.SERVER_SERVICE_UNAVAILABLE]: 503,
  [ErrorCodes.SERVER_TIMEOUT]: 503,
};
```

## Custom Exception Classes

```typescript
// apps/backend/src/exceptions/api.exception.ts

import { HttpException, HttpStatus } from '@nestjs/common';
import { ErrorCode, ErrorCodeToStatus, FieldError } from '@monorepo/shared-api';

export class ApiException extends HttpException {
  constructor(
    code: ErrorCode,
    message: string,
    errors?: FieldError[],
  ) {
    const status = ErrorCodeToStatus[code] || HttpStatus.INTERNAL_SERVER_ERROR;
    super(
      {
        code,
        message,
        errors,
      },
      status,
    );
  }
}

// 편의 클래스들
export class ValidationException extends ApiException {
  constructor(errors: FieldError[]) {
    super(
      ErrorCodes.VALIDATION_FAILED,
      'Validation failed',
      errors,
    );
  }
}

export class NotFoundException extends ApiException {
  constructor(resource: string, id?: string) {
    const message = id
      ? `${resource} with id '${id}' not found`
      : `${resource} not found`;
    super(ErrorCodes.RESOURCE_NOT_FOUND, message);
  }
}

export class UnauthorizedException extends ApiException {
  constructor(message = 'Unauthorized') {
    super(ErrorCodes.AUTH_UNAUTHORIZED, message);
  }
}

export class ForbiddenException extends ApiException {
  constructor(message = 'Access denied') {
    super(ErrorCodes.AUTH_FORBIDDEN, message);
  }
}

export class ConflictException extends ApiException {
  constructor(resource: string, field: string) {
    super(
      ErrorCodes.RESOURCE_CONFLICT,
      `${resource} with this ${field} already exists`,
    );
  }
}

// 사용 예시
throw new NotFoundException('User', userId);
throw new ConflictException('User', 'email');
throw new ValidationException([
  { field: 'email', message: 'Invalid email format', rule: 'isEmail' },
]);
```

## Global Exception Filter

```typescript
// apps/backend/src/filters/http-exception.filter.ts

import {
  ExceptionFilter,
  Catch,
  ArgumentsHost,
  HttpException,
  HttpStatus,
  Logger,
} from '@nestjs/common';
import { Request, Response } from 'express';
import { v4 as uuidv4 } from 'uuid';

@Catch()
export class GlobalExceptionFilter implements ExceptionFilter {
  private readonly logger = new Logger(GlobalExceptionFilter.name);

  catch(exception: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();

    const traceId = uuidv4();
    const timestamp = new Date().toISOString();

    let statusCode = HttpStatus.INTERNAL_SERVER_ERROR;
    let errorCode = ErrorCodes.SERVER_INTERNAL_ERROR;
    let message = 'Internal server error';
    let details: string | undefined;
    let errors: FieldError[] | undefined;

    if (exception instanceof HttpException) {
      statusCode = exception.getStatus();
      const exceptionResponse = exception.getResponse();

      if (typeof exceptionResponse === 'string') {
        message = exceptionResponse;
      } else if (typeof exceptionResponse === 'object') {
        const res = exceptionResponse as Record<string, any>;
        message = res.message || message;
        errorCode = res.code || this.statusToErrorCode(statusCode);
        errors = res.errors;
      }
    } else if (exception instanceof Error) {
      message = exception.message;
      details = process.env.NODE_ENV === 'development'
        ? exception.stack
        : undefined;
    }

    // 로깅
    this.logger.error(
      `[${traceId}] ${request.method} ${request.url} - ${statusCode}`,
      {
        traceId,
        statusCode,
        errorCode,
        message,
        path: request.url,
        method: request.method,
        ip: request.ip,
        userAgent: request.get('user-agent'),
        userId: (request as any).user?.id,
        stack: exception instanceof Error ? exception.stack : undefined,
      },
    );

    const errorResponse: ApiErrorResponse = {
      success: false,
      error: {
        statusCode,
        code: errorCode,
        message,
        details,
        errors,
        traceId,
      },
      timestamp,
    };

    response.status(statusCode).json(errorResponse);
  }

  private statusToErrorCode(status: number): string {
    const mapping: Record<number, string> = {
      400: ErrorCodes.VALIDATION_FAILED,
      401: ErrorCodes.AUTH_UNAUTHORIZED,
      403: ErrorCodes.AUTH_FORBIDDEN,
      404: ErrorCodes.RESOURCE_NOT_FOUND,
      409: ErrorCodes.RESOURCE_CONFLICT,
      429: ErrorCodes.RATE_LIMIT_EXCEEDED,
      500: ErrorCodes.SERVER_INTERNAL_ERROR,
      503: ErrorCodes.SERVER_SERVICE_UNAVAILABLE,
    };
    return mapping[status] || ErrorCodes.SERVER_INTERNAL_ERROR;
  }
}
```

```

### patterns/type-generation.md

```markdown
# Type Generation

## swagger-typescript-api 설정

```typescript
// packages/shared/api/scripts/generate-types.ts

import { generateApi } from 'swagger-typescript-api';
import * as path from 'path';
import * as fs from 'fs';

const OUTPUT_PATH = path.resolve(__dirname, '../generated');
const OPENAPI_PATH = path.resolve(__dirname, '../openapi/openapi.yaml');

async function generateTypes() {
  // 출력 디렉토리 정리
  if (fs.existsSync(OUTPUT_PATH)) {
    fs.rmSync(OUTPUT_PATH, { recursive: true });
  }
  fs.mkdirSync(OUTPUT_PATH, { recursive: true });

  await generateApi({
    name: 'api.ts',
    output: OUTPUT_PATH,
    input: OPENAPI_PATH,
    httpClientType: 'axios',
    generateClient: true,
    generateRouteTypes: true,
    generateResponses: true,
    extractRequestParams: true,
    extractRequestBody: true,
    extractEnums: true,
    unwrapResponseData: true,
    prettier: {
      printWidth: 100,
      tabWidth: 2,
      singleQuote: true,
      trailingComma: 'all',
    },
    hooks: {
      onFormatTypeName: (typeName: string) => typeName,
      onFormatRouteName: (routeInfo: any) => {
        return routeInfo.operationId || routeInfo.route;
      },
    },
  });

  console.log('Types generated at:', OUTPUT_PATH);
}

generateTypes().catch(console.error);
```

## package.json 스크립트

```json
{
  "name": "@monorepo/shared-api",
  "version": "1.0.0",
  "scripts": {
    "generate": "ts-node scripts/generate-types.ts",
    "generate:watch": "nodemon --watch openapi -e yaml,json --exec 'pnpm generate'",
    "validate": "openapi-generator-cli validate -i openapi/openapi.yaml",
    "lint": "spectral lint openapi/openapi.yaml"
  },
  "dependencies": {
    "axios": "^1.6.0",
    "zod": "^3.22.0"
  },
  "devDependencies": {
    "swagger-typescript-api": "^13.0.0",
    "@openapitools/openapi-generator-cli": "^2.7.0",
    "@stoplight/spectral-cli": "^6.11.0",
    "ts-node": "^10.9.0",
    "typescript": "^5.3.0",
    "nodemon": "^3.0.0"
  }
}
```

## 생성된 타입 활용

```typescript
// apps/web/src/lib/api-client.ts

import { Api, HttpClient } from '@monorepo/shared-api/generated';

// 기본 Axios 인스턴스 설정
const httpClient = new HttpClient({
  baseURL: process.env.NEXT_PUBLIC_API_URL,
  headers: {
    'Content-Type': 'application/json',
  },
});

// 인증 토큰 인터셉터
httpClient.instance.interceptors.request.use((config) => {
  const token = localStorage.getItem('accessToken');
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

// 에러 인터셉터
httpClient.instance.interceptors.response.use(
  (response) => response,
  async (error) => {
    if (error.response?.status === 401) {
      const refreshed = await refreshToken();
      if (refreshed) {
        return httpClient.instance(error.config);
      }
      logout();
    }
    return Promise.reject(error);
  },
);

// API 클라이언트 인스턴스
export const api = new Api(httpClient);

// 타입 안전한 사용 예시
async function fetchUser(userId: string) {
  const response = await api.users.getUserById(userId);
  return response.data; // UserDto 타입
}
```

## React Query 통합

```typescript
// apps/web/src/hooks/use-users.ts

import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { api } from '@/lib/api-client';
import type { CreateUserDto, UpdateUserDto, GetUsersQueryDto } from '@monorepo/shared-api';

// Query Keys 중앙 관리
export const userKeys = {
  all: ['users'] as const,
  lists: () => [...userKeys.all, 'list'] as const,
  list: (filters: GetUsersQueryDto) => [...userKeys.lists(), filters] as const,
  details: () => [...userKeys.all, 'detail'] as const,
  detail: (id: string) => [...userKeys.details(), id] as const,
};

/**
 * 사용자 목록 조회 훅
 */
export function useUsers(query: GetUsersQueryDto = {}) {
  return useQuery({
    queryKey: userKeys.list(query),
    queryFn: async () => {
      const response = await api.users.getUsers(query);
      return response.data;
    },
    staleTime: 5 * 60 * 1000, // 5분
  });
}

/**
 * 단일 사용자 조회 훅
 */
export function useUser(userId: string) {
  return useQuery({
    queryKey: userKeys.detail(userId),
    queryFn: async () => {
      const response = await api.users.getUserById(userId);
      return response.data;
    },
    enabled: !!userId,
  });
}

/**
 * 사용자 생성 뮤테이션
 */
export function useCreateUser() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: async (data: CreateUserDto) => {
      const response = await api.users.createUser(data);
      return response.data;
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: userKeys.lists() });
    },
  });
}

/**
 * 사용자 수정 뮤테이션
 */
export function useUpdateUser() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: async ({ id, data }: { id: string; data: UpdateUserDto }) => {
      const response = await api.users.updateUser(id, data);
      return response.data;
    },
    onSuccess: (_, { id }) => {
      queryClient.invalidateQueries({ queryKey: userKeys.detail(id) });
      queryClient.invalidateQueries({ queryKey: userKeys.lists() });
    },
  });
}

/**
 * 사용자 삭제 뮤테이션
 */
export function useDeleteUser() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: async (userId: string) => {
      await api.users.deleteUser(userId);
    },
    onSuccess: (_, userId) => {
      queryClient.removeQueries({ queryKey: userKeys.detail(userId) });
      queryClient.invalidateQueries({ queryKey: userKeys.lists() });
    },
  });
}
```

```

### templates/controller-template.md

```markdown
# Complete Controller Template

```typescript
// apps/backend/src/modules/users/users.controller.ts

import {
  Controller,
  Get,
  Post,
  Put,
  Delete,
  Body,
  Param,
  Query,
  UseGuards,
  HttpCode,
  HttpStatus,
  ParseUUIDPipe,
} from '@nestjs/common';
import {
  ApiTags,
  ApiOperation,
  ApiResponse,
  ApiParam,
  ApiQuery,
  ApiBearerAuth,
  ApiBody,
} from '@nestjs/swagger';
import { UsersService } from './users.service';
import {
  CreateUserDto,
  UpdateUserDto,
  UserDto,
  UserResponseDto,
  UsersListResponseDto,
  GetUsersQueryDto,
  ApiErrorResponseDto,
} from './dto';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import { RolesGuard } from '../auth/guards/roles.guard';
import { Roles } from '../auth/decorators/roles.decorator';
import { CurrentUser } from '../auth/decorators/current-user.decorator';
import { User, UserRole } from './entities/user.entity';
import { ApiStandardResponse, ApiPaginatedResponse } from '@/common/decorators';

@ApiTags('Users')
@ApiBearerAuth('access-token')
@Controller('users')
@UseGuards(JwtAuthGuard, RolesGuard)
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  // ============================================================
  // CREATE
  // ============================================================

  @Post()
  @Roles(UserRole.ADMIN)
  @HttpCode(HttpStatus.CREATED)
  @ApiOperation({
    summary: '사용자 생성',
    description: `
## 설명
새로운 사용자 계정을 생성합니다.

## 권한
- \`ADMIN\` 역할만 사용 가능

## 제약 조건
- 이메일은 시스템 내 유일해야 함
- 비밀번호: 8자 이상, 대소문자/숫자/특수문자 포함
    `,
  })
  @ApiBody({
    type: CreateUserDto,
    examples: {
      basic: {
        summary: '기본 사용자',
        value: {
          email: '[email protected]',
          password: 'SecurePass123!',
          name: '홍길동',
        },
      },
      admin: {
        summary: '관리자',
        value: {
          email: '[email protected]',
          password: 'AdminPass123!',
          name: '관리자',
          role: 'ADMIN',
        },
      },
    },
  })
  @ApiResponse({
    status: HttpStatus.CREATED,
    description: '사용자 생성 성공',
    type: UserResponseDto,
  })
  @ApiResponse({
    status: HttpStatus.BAD_REQUEST,
    description: '잘못된 요청 (검증 오류)',
    type: ApiErrorResponseDto,
  })
  @ApiResponse({
    status: HttpStatus.CONFLICT,
    description: '이메일 중복',
    type: ApiErrorResponseDto,
  })
  async create(@Body() createUserDto: CreateUserDto): Promise<UserResponseDto> {
    const user = await this.usersService.create(createUserDto);
    return this.wrapResponse(user);
  }

  // ============================================================
  // READ (List)
  // ============================================================

  @Get()
  @Roles(UserRole.ADMIN)
  @ApiOperation({
    summary: '사용자 목록 조회',
    description: '페이지네이션이 적용된 사용자 목록을 조회합니다.',
  })
  @ApiQuery({ name: 'page', type: Number, required: false, example: 1 })
  @ApiQuery({ name: 'limit', type: Number, required: false, example: 20 })
  @ApiQuery({ name: 'role', enum: UserRole, required: false })
  @ApiQuery({ name: 'search', type: String, required: false })
  @ApiPaginatedResponse(UserDto)
  @ApiResponse({
    status: HttpStatus.UNAUTHORIZED,
    description: '인증 실패',
    type: ApiErrorResponseDto,
  })
  @ApiResponse({
    status: HttpStatus.FORBIDDEN,
    description: '권한 없음',
    type: ApiErrorResponseDto,
  })
  async findAll(@Query() query: GetUsersQueryDto): Promise<UsersListResponseDto> {
    const { data, total } = await this.usersService.findAll(query);
    return this.wrapPaginatedResponse(data, query, total);
  }

  // ============================================================
  // READ (Single)
  // ============================================================

  @Get(':id')
  @ApiOperation({
    summary: '사용자 상세 조회',
    description: '특정 사용자의 상세 정보를 조회합니다.',
  })
  @ApiParam({
    name: 'id',
    type: String,
    format: 'uuid',
    description: '사용자 ID',
    example: '550e8400-e29b-41d4-a716-446655440000',
  })
  @ApiStandardResponse(UserDto)
  @ApiResponse({
    status: HttpStatus.NOT_FOUND,
    description: '사용자를 찾을 수 없음',
    type: ApiErrorResponseDto,
  })
  async findOne(
    @Param('id', ParseUUIDPipe) id: string,
    @CurrentUser() currentUser: User,
  ): Promise<UserResponseDto> {
    // 본인 또는 관리자만 조회 가능
    if (currentUser.role !== UserRole.ADMIN && currentUser.id !== id) {
      throw new ForbiddenException('자신의 정보만 조회할 수 있습니다');
    }
    const user = await this.usersService.findOne(id);
    return this.wrapResponse(user);
  }

  // ============================================================
  // UPDATE
  // ============================================================

  @Put(':id')
  @ApiOperation({
    summary: '사용자 정보 수정',
    description: '사용자 정보를 수정합니다.',
  })
  @ApiParam({
    name: 'id',
    type: String,
    format: 'uuid',
    description: '사용자 ID',
  })
  @ApiBody({ type: UpdateUserDto })
  @ApiStandardResponse(UserDto)
  @ApiResponse({
    status: HttpStatus.NOT_FOUND,
    description: '사용자를 찾을 수 없음',
    type: ApiErrorResponseDto,
  })
  @ApiResponse({
    status: HttpStatus.CONFLICT,
    description: '이메일 중복',
    type: ApiErrorResponseDto,
  })
  async update(
    @Param('id', ParseUUIDPipe) id: string,
    @Body() updateUserDto: UpdateUserDto,
    @CurrentUser() currentUser: User,
  ): Promise<UserResponseDto> {
    // 본인 또는 관리자만 수정 가능
    if (currentUser.role !== UserRole.ADMIN && currentUser.id !== id) {
      throw new ForbiddenException('자신의 정보만 수정할 수 있습니다');
    }
    // 일반 사용자는 역할 변경 불가
    if (currentUser.role !== UserRole.ADMIN && updateUserDto.role) {
      throw new ForbiddenException('역할을 변경할 권한이 없습니다');
    }
    const user = await this.usersService.update(id, updateUserDto);
    return this.wrapResponse(user);
  }

  // ============================================================
  // DELETE
  // ============================================================

  @Delete(':id')
  @Roles(UserRole.ADMIN)
  @HttpCode(HttpStatus.NO_CONTENT)
  @ApiOperation({
    summary: '사용자 삭제',
    description: '사용자 계정을 삭제합니다 (Soft Delete).',
  })
  @ApiParam({
    name: 'id',
    type: String,
    format: 'uuid',
    description: '사용자 ID',
  })
  @ApiResponse({
    status: HttpStatus.NO_CONTENT,
    description: '삭제 성공',
  })
  @ApiResponse({
    status: HttpStatus.NOT_FOUND,
    description: '사용자를 찾을 수 없음',
    type: ApiErrorResponseDto,
  })
  async remove(@Param('id', ParseUUIDPipe) id: string): Promise<void> {
    await this.usersService.remove(id);
  }

  // ============================================================
  // Helper Methods
  // ============================================================

  private wrapResponse(user: User): UserResponseDto {
    return {
      success: true,
      data: this.toDto(user),
      timestamp: new Date().toISOString(),
    };
  }

  private wrapPaginatedResponse(
    users: User[],
    query: GetUsersQueryDto,
    total: number,
  ): UsersListResponseDto {
    const page = query.page || 1;
    const limit = query.limit || 20;
    const totalPages = Math.ceil(total / limit);

    return {
      success: true,
      data: users.map((u) => this.toDto(u)),
      meta: {
        page,
        limit,
        total,
        totalPages,
        hasNext: page < totalPages,
        hasPrev: page > 1,
      },
      timestamp: new Date().toISOString(),
    };
  }

  private toDto(user: User): UserDto {
    return {
      id: user.id,
      email: user.email,
      name: user.name,
      phone: user.phone,
      role: user.role,
      createdAt: user.createdAt.toISOString(),
      updatedAt: user.updatedAt.toISOString(),
    };
  }
}
```

```

### templates/openapi-yaml.md

```markdown
# OpenAPI YAML Templates

## Main OpenAPI Spec

```yaml
# packages/shared/api/openapi/openapi.yaml

openapi: 3.0.3
info:
  title: MonoRepo API
  description: |
    # MonoRepo API Documentation

    ## Authentication
    모든 API (인증 관련 제외)는 Bearer Token이 필요합니다.

    ## Response Format
    모든 응답은 표준 포맷을 따릅니다.
  version: 1.0.0
  contact:
    name: Backend Team
    email: [email protected]
  license:
    name: MIT

servers:
  - url: http://localhost:3000/api/v1
    description: Local Development
  - url: https://api.staging.example.com/v1
    description: Staging
  - url: https://api.example.com/v1
    description: Production

tags:
  - name: Auth
    description: 인증 관련 API
  - name: Users
    description: 사용자 관련 API

paths:
  /auth/login:
    $ref: './paths/auth.yaml#/login'
  /auth/refresh:
    $ref: './paths/auth.yaml#/refresh'
  /users:
    $ref: './paths/users.yaml#/users'
  /users/{id}:
    $ref: './paths/users.yaml#/users-id'

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: JWT 토큰을 입력하세요

  schemas:
    $ref: './components/schemas.yaml'

  responses:
    $ref: './components/responses.yaml'

  parameters:
    $ref: './components/parameters.yaml'

security:
  - bearerAuth: []
```

## Users Path Definition

```yaml
# packages/shared/api/openapi/paths/users.yaml

users:
  get:
    tags:
      - Users
    operationId: getUsers
    summary: 사용자 목록 조회
    description: 페이지네이션이 적용된 사용자 목록을 조회합니다.
    parameters:
      - $ref: '../components/parameters.yaml#/PageParam'
      - $ref: '../components/parameters.yaml#/LimitParam'
      - name: role
        in: query
        schema:
          $ref: '../components/schemas.yaml#/UserRole'
      - name: search
        in: query
        schema:
          type: string
    responses:
      '200':
        description: 성공
        content:
          application/json:
            schema:
              $ref: '../components/responses.yaml#/UsersListResponse'
      '401':
        $ref: '../components/responses.yaml#/UnauthorizedError'
      '403':
        $ref: '../components/responses.yaml#/ForbiddenError'

  post:
    tags:
      - Users
    operationId: createUser
    summary: 사용자 생성
    requestBody:
      required: true
      content:
        application/json:
          schema:
            $ref: '../components/schemas.yaml#/CreateUserDto'
          examples:
            basic:
              summary: 기본 사용자
              value:
                email: [email protected]
                password: SecurePass123!
                name: 홍길동
    responses:
      '201':
        description: 생성 성공
        content:
          application/json:
            schema:
              $ref: '../components/responses.yaml#/UserResponse'
      '400':
        $ref: '../components/responses.yaml#/ValidationError'
      '409':
        $ref: '../components/responses.yaml#/ConflictError'

users-id:
  get:
    tags:
      - Users
    operationId: getUserById
    summary: 사용자 상세 조회
    parameters:
      - $ref: '../components/parameters.yaml#/UserIdParam'
    responses:
      '200':
        description: 성공
        content:
          application/json:
            schema:
              $ref: '../components/responses.yaml#/UserResponse'
      '404':
        $ref: '../components/responses.yaml#/NotFoundError'

  put:
    tags:
      - Users
    operationId: updateUser
    summary: 사용자 정보 수정
    parameters:
      - $ref: '../components/parameters.yaml#/UserIdParam'
    requestBody:
      required: true
      content:
        application/json:
          schema:
            $ref: '../components/schemas.yaml#/UpdateUserDto'
    responses:
      '200':
        description: 수정 성공
        content:
          application/json:
            schema:
              $ref: '../components/responses.yaml#/UserResponse'
      '404':
        $ref: '../components/responses.yaml#/NotFoundError'
      '409':
        $ref: '../components/responses.yaml#/ConflictError'

  delete:
    tags:
      - Users
    operationId: deleteUser
    summary: 사용자 삭제
    parameters:
      - $ref: '../components/parameters.yaml#/UserIdParam'
    responses:
      '204':
        description: 삭제 성공
      '404':
        $ref: '../components/responses.yaml#/NotFoundError'
```

## Schemas Component

```yaml
# packages/shared/api/openapi/components/schemas.yaml

UserRole:
  type: string
  enum:
    - USER
    - ADMIN
  description: 사용자 역할

UserDto:
  type: object
  required:
    - id
    - email
    - name
    - role
    - createdAt
    - updatedAt
  properties:
    id:
      type: string
      format: uuid
      example: '550e8400-e29b-41d4-a716-446655440000'
    email:
      type: string
      format: email
      example: '[email protected]'
    name:
      type: string
      example: '홍길동'
    phone:
      type: string
      example: '010-1234-5678'
    role:
      $ref: '#/UserRole'
    createdAt:
      type: string
      format: date-time
    updatedAt:
      type: string
      format: date-time

CreateUserDto:
  type: object
  required:
    - email
    - password
    - name
  properties:
    email:
      type: string
      format: email
      maxLength: 255
    password:
      type: string
      minLength: 8
      maxLength: 100
    name:
      type: string
      minLength: 2
      maxLength: 50
    phone:
      type: string
      pattern: '^01[0-9]-\d{3,4}-\d{4}$'
    role:
      $ref: '#/UserRole'

UpdateUserDto:
  type: object
  properties:
    name:
      type: string
      minLength: 2
      maxLength: 50
    phone:
      type: string
      pattern: '^01[0-9]-\d{3,4}-\d{4}$'
    role:
      $ref: '#/UserRole'
```

## Parameters Component

```yaml
# packages/shared/api/openapi/components/parameters.yaml

PageParam:
  name: page
  in: query
  required: false
  schema:
    type: integer
    minimum: 1
    default: 1
  description: 페이지 번호 (1부터 시작)

LimitParam:
  name: limit
  in: query
  required: false
  schema:
    type: integer
    minimum: 1
    maximum: 100
    default: 20
  description: 페이지당 항목 수

UserIdParam:
  name: id
  in: path
  required: true
  schema:
    type: string
    format: uuid
  description: 사용자 ID
  example: '550e8400-e29b-41d4-a716-446655440000'
```

## Responses Component

```yaml
# packages/shared/api/openapi/components/responses.yaml

UserResponse:
  type: object
  properties:
    success:
      type: boolean
      example: true
    data:
      $ref: './schemas.yaml#/UserDto'
    timestamp:
      type: string
      format: date-time

UsersListResponse:
  type: object
  properties:
    success:
      type: boolean
      example: true
    data:
      type: array
      items:
        $ref: './schemas.yaml#/UserDto'
    meta:
      type: object
      properties:
        page:
          type: integer
          example: 1
        limit:
          type: integer
          example: 20
        total:
          type: integer
          example: 100
        totalPages:
          type: integer
          example: 5
        hasNext:
          type: boolean
          example: true
        hasPrev:
          type: boolean
          example: false
    timestamp:
      type: string
      format: date-time

UnauthorizedError:
  description: 인증 실패
  content:
    application/json:
      schema:
        $ref: '#/ApiErrorResponse'

ForbiddenError:
  description: 권한 없음
  content:
    application/json:
      schema:
        $ref: '#/ApiErrorResponse'

NotFoundError:
  description: 리소스를 찾을 수 없음
  content:
    application/json:
      schema:
        $ref: '#/ApiErrorResponse'

ValidationError:
  description: 검증 오류
  content:
    application/json:
      schema:
        $ref: '#/ApiErrorResponse'

ConflictError:
  description: 리소스 충돌
  content:
    application/json:
      schema:
        $ref: '#/ApiErrorResponse'

ApiErrorResponse:
  type: object
  properties:
    success:
      type: boolean
      example: false
    error:
      type: object
      properties:
        statusCode:
          type: integer
        code:
          type: string
        message:
          type: string
        traceId:
          type: string
        errors:
          type: array
          items:
            type: object
            properties:
              field:
                type: string
              message:
                type: string
    timestamp:
      type: string
      format: date-time
```

```

### quick-reference/checklist.md

```markdown
# API Development Checklist

## API 개발 전 체크리스트

```markdown
## 개발 전 (Contract First)

- [ ] OpenAPI 스펙 정의 완료
- [ ] Frontend/Backend 스펙 합의 완료
- [ ] 에러 케이스 정의 완료
- [ ] 인증/권한 요구사항 정의
- [ ] 페이지네이션 전략 결정
- [ ] 타입 생성 (swagger-typescript-api)
```

## Controller 구현 체크리스트

```markdown
## Controller 구현

- [ ] @ApiTags 적용 (복수형)
- [ ] @ApiBearerAuth 적용 (인증 필요 시)
- [ ] 모든 메서드에 @ApiOperation
  - [ ] summary (50자 이내)
  - [ ] description (마크다운, 상세)
- [ ] 모든 메서드에 @ApiResponse
  - [ ] 200/201: 성공
  - [ ] 400: 검증 오류
  - [ ] 401: 인증 오류
  - [ ] 403: 권한 오류
  - [ ] 404: 리소스 없음
  - [ ] 409: 충돌
- [ ] @ApiParam: 모든 Path Parameter
- [ ] @ApiQuery: 모든 Query Parameter
- [ ] @ApiBody: Request Body (예시 포함)
```

## DTO 체크리스트

```markdown
## DTO 정의

- [ ] @ApiProperty 모든 필수 필드
- [ ] @ApiPropertyOptional 모든 선택 필드
- [ ] description 작성
- [ ] example 제공
- [ ] format 지정 (email, uuid, date-time 등)
- [ ] 검증 데코레이터와 일치 (minLength, pattern 등)
- [ ] class-validator 데코레이터 적용
```

## 응답 포맷 체크리스트

```markdown
## 응답 포맷

- [ ] 성공: { success: true, data, meta?, timestamp }
- [ ] 에러: { success: false, error, timestamp }
- [ ] error.code는 ErrorCodes enum 사용
- [ ] error.traceId 포함 (로그 추적용)
- [ ] 검증 에러: error.errors 배열 포함
- [ ] 페이지네이션: meta 객체 포함
```

## 배포 전 체크리스트

```markdown
## 배포 전

- [ ] OpenAPI 스펙 유효성 검사 (openapi-generator validate)
- [ ] Swagger UI 확인
- [ ] 타입 재생성 및 프론트엔드 배포
- [ ] API 버전 확인
- [ ] 이전 버전 호환성 확인 (Breaking Change 시)
- [ ] Deprecation 헤더 설정 (구버전)
```

## Quick Reference

```yaml
표준_응답:
  성공: "{ success: true, data: T, meta?: Meta, timestamp: string }"
  에러: "{ success: false, error: ApiError, timestamp: string }"

필수_데코레이터:
  Controller:
    - "@ApiTags('Resources')"
    - "@ApiBearerAuth('access-token')"

  Method:
    - "@ApiOperation({ summary, description })"
    - "@ApiResponse({ status, description, type })"
    - "@ApiParam / @ApiQuery / @ApiBody"

  DTO:
    - "@ApiProperty({ description, example, format })"
    - "@ApiPropertyOptional()"

에러_코드_체계:
  패턴: "DOMAIN_ACTION_REASON"
  예시:
    - "AUTH_TOKEN_EXPIRED"
    - "USER_NOT_FOUND"
    - "VALIDATION_FAILED"

버전_전략:
  형식: "/api/v1/resources"
  지원: "현재 버전 + 이전 버전(12개월)"
  Breaking: "새 버전으로 분리"
```

## Documentation Rules

```yaml
모든 API는 다음 항목을 반드시 문서화:

Controller_Level:
  - "@ApiTags": 그룹명 (복수형)
  - "@ApiBearerAuth": 인증 필요 시

Method_Level:
  - "@ApiOperation":
      - summary: 한 줄 설명 (50자 이내)
      - description: 상세 설명 (마크다운)
  - "@ApiResponse": 모든 가능한 상태 코드
  - "@ApiParam": Path 파라미터
  - "@ApiQuery": Query 파라미터
  - "@ApiBody": Request Body (예시 포함)

DTO_Level:
  - "@ApiProperty": 모든 필수 속성
  - "@ApiPropertyOptional": 모든 선택 속성
  - description: 속성 설명
  - example: 예시 값
  - format: 형식 (email, uuid, date-time 등)
```

```

### quick-reference/anti-patterns.md

```markdown
# API Design Anti-Patterns

## 피해야 할 패턴들

### 1. 불완전한 Swagger 문서화

```typescript
// ❌ Bad
@Get()
getUsers() {}  // 데코레이터 없음!

// ✅ Good
@Get()
@ApiOperation({ summary: '사용자 목록 조회' })
@ApiResponse({ status: 200, type: UsersListResponseDto })
@ApiResponse({ status: 401, type: ApiErrorResponseDto })
getUsers() {}
```

### 2. 일관성 없는 응답 포맷

```typescript
// ❌ Bad
return { user, success: true };       // 때로는 user
return { data: user };                 // 때로는 data
return user;                           // 때로는 직접

// ✅ Good: 항상 표준 포맷 사용
return {
  success: true,
  data: user,
  timestamp: new Date().toISOString(),
};
```

### 3. any 타입 사용

```typescript
// ❌ Bad
@ApiProperty()
data: any;

// ✅ Good
@ApiProperty({ type: UserDto })
data: UserDto;
```

### 4. 예시 없는 DTO

```typescript
// ❌ Bad
@ApiProperty()
email: string;

// ✅ Good
@ApiProperty({
  description: '사용자 이메일',
  example: '[email protected]',
  format: 'email',
})
email: string;
```

### 5. 에러 응답 미정의

```typescript
// ❌ Bad
@ApiResponse({ status: 200, type: UserDto })
getUser() {}

// ✅ Good
@ApiResponse({ status: 200, type: UserDto })
@ApiResponse({ status: 400, type: ApiErrorResponseDto })
@ApiResponse({ status: 401, type: ApiErrorResponseDto })
@ApiResponse({ status: 404, type: ApiErrorResponseDto })
getUser() {}
```

### 6. 직접 에러 throw

```typescript
// ❌ Bad
throw new Error('User not found');

// ✅ Good
throw new NotFoundException('User', userId);
```

### 7. 하드코딩된 에러 메시지/코드

```typescript
// ❌ Bad
return { error: { code: 'NOT_FOUND', message: '...' } };

// ✅ Good
throw new ApiException(ErrorCodes.USER_NOT_FOUND, message);
```

### 8. 버전 없는 API

```typescript
// ❌ Bad
@Controller('users')

// ✅ Good
@Controller({ path: 'users', version: '1' })
```

### 9. 문서와 코드 불일치

```typescript
// ❌ Bad
@ApiProperty({ required: false })  // 선택적
email: string;                      // 코드에서는 필수

// ✅ Good
@ApiPropertyOptional()
email?: string;
```

### 10. 검증과 문서 불일치

```typescript
// ❌ Bad
@ApiProperty({ minLength: 8 })
@MinLength(6)  // 서로 다른 값!
password: string;

// ✅ Good
@ApiProperty({ minLength: 8 })
@MinLength(8)
password: string;
```

## Anti-Pattern 탐지 정규식

```typescript
const API_DESIGN_ANTIPATTERNS = [
  // Swagger 문서화 누락
  {
    pattern: /@(Get|Post|Put|Patch|Delete)\([^)]*\)\s*\n\s*(?!@Api)/,
    message: 'HTTP 메서드에 Swagger 데코레이터 누락',
    severity: 'error',
  },

  // any 타입 사용
  {
    pattern: /@ApiProperty\([^)]*\)\s*\n\s*\w+:\s*any/,
    message: 'ApiProperty에 any 타입 사용 금지',
    severity: 'error',
  },

  // 예시 없는 ApiProperty
  {
    pattern: /@ApiProperty\(\s*\)\s*\n/,
    message: 'ApiProperty에 description과 example 필수',
    severity: 'warning',
  },

  // 직접 Error throw
  {
    pattern: /throw new Error\(/,
    message: 'Error 대신 커스텀 예외 클래스 사용',
    severity: 'error',
  },

  // 하드코딩된 에러 코드
  {
    pattern: /code:\s*['"`][A-Z_]+['"`]/,
    message: 'ErrorCodes enum 사용 필요',
    severity: 'warning',
  },

  // 일관성 없는 응답
  {
    pattern: /return\s*\{\s*(?!success|data)/,
    message: '표준 응답 포맷 사용 필요',
    severity: 'warning',
  },
];
```

## Quick Detection Commands

```bash
# Swagger 데코레이터 없는 메서드 찾기
grep -rn "@(Get|Post|Put|Delete)" --include="*.controller.ts" | \
  grep -v "@Api"

# any 타입 사용 찾기
grep -rn ": any" --include="*.dto.ts"

# throw new Error 찾기
grep -rn "throw new Error" --include="*.ts"

# 하드코딩된 에러 코드 찾기
grep -rn "code: ['\"]" --include="*.ts"
```

```

api-first-design | SkillHub