AgentSkillsCN

api-design-guide

RESTful API设计指南——打造直观且一致的API

SKILL.md
--- frontmatter
name: api-design-guide
description: RESTful API 설계 가이드 - 직관적이고 일관성 있는 API 만들기

API 설계 가이드

핵심 원칙

1. 명확하고 예측 가능하게

API를 처음 보는 개발자가 문서 없이도 80% 이해할 수 있어야 합니다.

2. 일관성이 최우선

좋은 규칙을 일관되게 따르는 것이, 완벽한 규칙을 불규칙하게 따르는 것보다 낫습니다.

3. 개발자 경험(DX) 중심

API는 개발자를 위한 제품입니다. 사용하기 쉬워야 합니다.


URL 설계 원칙

✅ 좋은 URL 설계

code
# 명사 사용 (동사 X)
GET    /users              # ✅
GET    /getUsers           # ❌

# 복수형 사용
GET    /users              # ✅
GET    /user               # ❌

# 계층 구조 표현
GET    /users/123/orders   # ✅
GET    /orders?userId=123  # 😐 (대안으로 허용)

# 케밥 케이스 사용
GET    /user-profiles      # ✅
GET    /userProfiles       # ❌
GET    /user_profiles      # ❌

# 필터는 쿼리 파라미터로
GET    /products?category=electronics&sort=price  # ✅
GET    /products/electronics/sortByPrice          # ❌

❌ 피해야 할 패턴

code
# URL에 동사 포함
POST   /users/create       # ❌
POST   /users              # ✅

# URL에 동작 표현
GET    /users/search       # ❌
GET    /users?q=keyword    # ✅

# 파일 확장자 포함
GET    /users.json         # ❌
GET    /users              # ✅ (Accept 헤더 사용)

HTTP 메서드 사용법

CRUD 매핑

code
# Create - POST
POST   /users              # 새 사용자 생성
Body: { "name": "홍길동", "email": "hong@example.com" }

# Read - GET
GET    /users              # 목록 조회
GET    /users/123          # 단건 조회

# Update - PUT/PATCH
PUT    /users/123          # 전체 수정 (모든 필드 필요)
PATCH  /users/123          # 부분 수정 (일부 필드만)
Body: { "name": "김철수" }

# Delete - DELETE
DELETE /users/123          # 삭제

멱등성(Idempotency) 이해하기

code
# 멱등성 O (여러 번 호출해도 같은 결과)
GET    /users/123          # ✅ 항상 같은 결과
PUT    /users/123          # ✅ 같은 데이터로 여러 번 호출해도 같은 상태
DELETE /users/123          # ✅ 두 번째부터는 404지만 상태는 같음

# 멱등성 X (호출할 때마다 결과가 다름)
POST   /users              # ❌ 호출할 때마다 새 사용자 생성
PATCH  /orders/123/add-item # ❌ 호출할 때마다 아이템 추가

상태 코드 전략

자주 사용하는 코드만 정확히

code
# 성공 (2xx)
200 OK                  # GET, PUT, PATCH 성공
201 Created             # POST 성공 (Location 헤더와 함께)
204 No Content          # DELETE 성공 (응답 body 없음)

# 클라이언트 오류 (4xx)
400 Bad Request         # 잘못된 요청 (validation 실패)
401 Unauthorized        # 인증 필요
403 Forbidden           # 권한 없음
404 Not Found           # 리소스 없음
409 Conflict            # 중복 등 충돌

# 서버 오류 (5xx)
500 Internal Server Error  # 서버 오류
503 Service Unavailable    # 일시적 서비스 불가

❌ 과도한 상태 코드 사용 피하기

code
# 불필요한 세분화
451 Unavailable For Legal Reasons  # ❌ 너무 구체적
403 Forbidden                       # ✅ 충분함

# 일관성 없는 사용
POST /users → 200 OK (이미 있음)    # ❌
POST /users → 409 Conflict          # ✅

응답 형식

일관된 JSON 구조

json
// ✅ 성공 응답
{
  "data": {
    "id": "123",
    "name": "홍길동",
    "email": "hong@example.com"
  }
}

// ✅ 목록 응답
{
  "data": [
    { "id": "1", "name": "홍길동" },
    { "id": "2", "name": "김철수" }
  ],
  "pagination": {
    "page": 1,
    "pageSize": 20,
    "totalCount": 100,
    "totalPages": 5
  }
}

// ✅ 에러 응답
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "이메일 형식이 올바르지 않습니다",
    "details": [
      {
        "field": "email",
        "message": "유효한 이메일을 입력하세요"
      }
    ]
  }
}

❌ 피해야 할 패턴

json
// ❌ 불일치한 구조
// 성공 시
{ "user": { "id": 1 } }

// 실패 시
{ "error": "..." }

// ✅ 일관된 구조
// 항상 data 또는 error 키 사용

페이지네이션

오프셋 기반 (간단한 경우)

code
GET /users?page=2&pageSize=20

Response:
{
  "data": [...],
  "pagination": {
    "page": 2,
    "pageSize": 20,
    "totalCount": 150,
    "totalPages": 8,
    "hasNext": true,
    "hasPrev": true
  }
}

커서 기반 (대용량/실시간)

code
GET /posts?cursor=eyJpZCI6MTIzfQ&limit=20

Response:
{
  "data": [...],
  "pagination": {
    "nextCursor": "eyJpZCI6MTQzfQ",
    "prevCursor": "eyJpZCI6MTAzfQ",
    "hasMore": true
  }
}

필터링 & 정렬

쿼리 파라미터 설계

code
# 필터링
GET /products?category=electronics&minPrice=10000&maxPrice=50000

# 정렬
GET /products?sort=price          # 오름차순
GET /products?sort=-price         # 내림차순 (- 접두사)
GET /products?sort=price,-rating  # 다중 정렬

# 필드 선택 (Sparse Fieldsets)
GET /users?fields=id,name,email   # 필요한 필드만

# 검색
GET /products?q=노트북             # 통합 검색
GET /products?search=name:노트북   # 필드별 검색

인증 & 보안

Bearer Token 사용

code
# 헤더에 토큰 전달
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

# ❌ URL에 토큰 포함하지 않기
GET /users?token=xxx  # 위험! 로그에 남음

API 키 (공개 API)

code
# 헤더 사용 (권장)
X-API-Key: your-api-key-here

# 또는 쿼리 파라미터 (읽기 전용 API만)
GET /public/data?apiKey=xxx

버전 관리

URL 버전 (가장 명확)

code
GET /v1/users
GET /v2/users

# 장점: 명확하고 캐싱 쉬움
# 단점: URL이 길어짐

헤더 버전 (RESTful)

code
GET /users
Accept: application/vnd.myapp.v2+json

# 장점: URL 깔끔
# 단점: 브라우저 테스트 어려움

⚠️ 버전 변경 원칙

code
# 하위 호환성 유지
v1: { "name": "홍길동" }
v2: { "name": "홍길동", "firstName": "길동", "lastName": "홍" }  # ✅

v1: { "name": "홍길동" }
v2: { "fullName": "홍길동" }  # ❌ name 필드 제거 (breaking change)

관계 표현

중첩 리소스

code
# 사용자의 주문 조회
GET /users/123/orders

# 특정 주문 상세
GET /users/123/orders/456

독립 리소스 + 필터

code
# 모든 주문에서 필터링
GET /orders?userId=123

# 언제 사용?
# - 리소스가 독립적으로 관리될 때
# - 다양한 필터링이 필요할 때

에러 처리

명확한 에러 메시지

json
// ✅ 좋은 에러 응답
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "입력값을 확인해주세요",
    "details": [
      {
        "field": "email",
        "code": "INVALID_FORMAT",
        "message": "올바른 이메일 형식이 아닙니다"
      },
      {
        "field": "password",
        "code": "TOO_SHORT",
        "message": "비밀번호는 8자 이상이어야 합니다"
      }
    ],
    "requestId": "req_123abc"
  }
}

// ❌ 나쁜 에러 응답
{
  "error": "Error"  // 너무 모호함
}

에러 코드 체계

code
# 명명 규칙
VALIDATION_ERROR
RESOURCE_NOT_FOUND
AUTHENTICATION_REQUIRED
INSUFFICIENT_PERMISSIONS
RATE_LIMIT_EXCEEDED

# ❌ 피할 것
ERR_001  # 의미 불명확
error    # 너무 일반적

Rate Limiting

헤더로 정보 제공

code
HTTP/1.1 200 OK
X-RateLimit-Limit: 100        # 시간당 한도
X-RateLimit-Remaining: 87     # 남은 요청 수
X-RateLimit-Reset: 1640000000 # 리셋 시각 (Unix timestamp)

# 한도 초과 시
HTTP/1.1 429 Too Many Requests
Retry-After: 3600  # 초 단위

실전 체크리스트

API 설계 전 확인

  • URL이 명사로만 구성되어 있는가?
  • 복수형을 일관되게 사용하는가?
  • HTTP 메서드를 올바르게 사용하는가?
  • 상태 코드가 의미에 맞는가?
  • 응답 구조가 일관적인가?
  • 에러 메시지가 명확한가?
  • 페이지네이션이 구현되어 있는가?
  • 인증 방식이 안전한가?
  • 버전 관리 전략이 있는가?
  • Rate limiting이 구현되어 있는가?

문서화 필수 항목

  • 모든 엔드포인트 목록
  • 요청/응답 예시
  • 에러 코드 설명
  • 인증 방법
  • Rate limit 정책

나쁜 예 vs 좋은 예

케이스 1: 사용자 관리

code
# ❌ 나쁜 설계
GET  /getUser?id=123
POST /createUser
POST /updateUser
POST /deleteUser

# ✅ 좋은 설계
GET    /users/123
POST   /users
PUT    /users/123
DELETE /users/123

케이스 2: 검색

code
# ❌ 나쁜 설계
GET /search/users/홍길동

# ✅ 좋은 설계
GET /users?q=홍길동

케이스 3: 복잡한 작업

code
# ❌ 나쁜 설계
POST /orders/123/sendEmail

# ✅ 좋은 설계
POST /orders/123/notifications
Body: { "type": "email" }

# 또는 이벤트 기반
POST /orders/123/confirm  # 확인하면 자동으로 이메일 발송

한국 서비스 특화

한글 지원

code
# URL 인코딩
GET /products?q=%ED%99%8D%EA%B8%B8%EB%8F%99

# Accept-Language 헤더
GET /products
Accept-Language: ko-KR

# 응답에 언어 포함
{
  "data": {
    "name": "상품명",
    "nameEn": "Product Name"
  }
}

주민등록번호 등 민감정보

code
# ❌ URL에 포함하지 않기
GET /users?residentNumber=123456-1234567

# ✅ POST body에 포함
POST /users/verify
Body: { "residentNumber": "123456-1234567" }

# 응답에서 마스킹
{
  "residentNumber": "123456-1******"
}

마무리 원칙

"API는 계약이다. 한 번 배포하면 쉽게 바꿀 수 없다."

  • 일관성: 같은 패턴을 반복하라
  • 예측 가능성: 놀라움을 주지 마라
  • 명확성: 문서 없이도 이해할 수 있게
  • 하위 호환성: 기존 클라이언트를 망가뜨리지 마라
  • 단순함: 복잡한 것보다 단순한 것이 낫다