Skip to content

Split View: REST API 설계 베스트 프랙티스 2025: 네이밍, 버저닝, 에러 처리, 페이지네이션, 보안

✨ Learn with Quiz
|

REST API 설계 베스트 프랙티스 2025: 네이밍, 버저닝, 에러 처리, 페이지네이션, 보안

서론

REST API는 현대 웹 서비스의 근간입니다. 모바일 앱, SPA, 마이크로서비스, IoT 기기 등 거의 모든 소프트웨어가 REST API를 통해 통신합니다. 하지만 "RESTful"이라고 주장하면서도 실제로는 REST 원칙을 위반하는 API가 놀라울 정도로 많습니다.

잘 설계된 API는 개발자 경험(DX)을 향상시키고, 유지보수를 쉽게 하며, 확장성과 보안을 보장합니다. 반대로 잘못 설계된 API는 팀 간 소통 비용을 증가시키고, 보안 취약점을 만들며, 기술 부채를 쌓습니다.

이 가이드에서는 리소스 네이밍부터 버저닝, 에러 처리, 인증, OpenAPI까지 REST API 설계의 모든 베스트 프랙티스를 다룹니다.


1. REST 원칙 (Principles)

1.1 REST의 6가지 제약 조건

1. Client-Server (클라이언트-서버 분리)
   - UI와 데이터 저장 관심사 분리
   - 독립적 진화 가능

2. Stateless (무상태)
   - 각 요청은 완전한 정보를 포함
   - 서버는 클라이언트 상태를 저장하지 않음
   - 확장성 향상

3. Cacheable (캐시 가능)
   - 응답에 캐시 가능 여부 명시
   - Cache-Control, ETag, Last-Modified 헤더 활용

4. Uniform Interface (통일된 인터페이스)
   - 리소스 식별 (URI)
   - 표현을 통한 리소스 조작
   - 자기 서술적 메시지
   - HATEOAS

5. Layered System (계층 구조)
   - 클라이언트는 직접 서버와 통신하는지 중간 계층과 통신하는지 알 수 없음
   - 로드 밸런서, 캐시, 게이트웨이 등 추가 가능

6. Code-on-Demand (선택 사항)
   - 서버가 실행 가능한 코드를 클라이언트에 전송 가능
   - 유일한 선택적 제약

1.2 REST의 성숙도 모델 (Richardson Maturity Model)

Level 3: Hypermedia Controls (HATEOAS)
  └── 응답에 관련 리소스 링크 포함
Level 2: HTTP Methods
  └── GET, POST, PUT, DELETE 올바른 사용
Level 1: Resources
  └── 개별 리소스에 URI 부여
Level 0: The Swamp of POX
  └── 단일 엔드포인트, 모든 작업을 POST
대부분의 APILevel 2를 목표로 합니다.
Level 3 (HATEOAS)는 이상적이지만 실무에서는 선택적입니다.

2. 리소스 네이밍 (Resource Naming)

2.1 기본 규칙

Good:
GET    /users               # 사용자 목록
GET    /users/123            # 특정 사용자
POST   /users               # 사용자 생성
PUT    /users/123            # 사용자 전체 수정
PATCH  /users/123            # 사용자 부분 수정
DELETE /users/123            # 사용자 삭제

Bad:
GET    /getUsers             # 동사 사용 금지
GET    /user/123             # 복수형 사용
POST   /createUser           # 동사 사용 금지
POST   /user/123/delete      # HTTP 메서드 활용

2.2 계층 관계 표현

# 사용자의 주문 목록
GET /users/123/orders

# 사용자의 특정 주문
GET /users/123/orders/456

# 주문의 아이템 목록
GET /users/123/orders/456/items

# 주의: 3단계 이상 중첩은 피하기
# Bad:
GET /users/123/orders/456/items/789/reviews

# Good (대안):
GET /items/789/reviews
GET /reviews?item_id=789

2.3 네이밍 컨벤션

# 소문자 + 하이픈 (kebab-case) 사용
Good: /user-profiles
Bad:  /userProfiles, /user_profiles, /UserProfiles

# 파일 확장자 사용 금지
Good: Accept: application/json
Bad:  /users/123.json

# 후행 슬래시 사용 금지
Good: /users
Bad:  /users/

# 필터링은 쿼리 파라미터로
GET /users?status=active&role=admin
GET /products?category=electronics&min_price=100&max_price=500

# 정렬
GET /users?sort=created_at&order=desc
GET /users?sort=-created_at,+name  # - 내림차순, + 오름차순

# 필드 선택 (Sparse Fieldsets)
GET /users/123?fields=name,email,avatar

2.4 리소스가 아닌 것들 처리

# 동작(Action)이 필요한 경우: 하위 리소스로 표현
POST /users/123/activate          # 사용자 활성화
POST /users/123/deactivate        # 사용자 비활성화
POST /orders/456/cancel           # 주문 취소
POST /payments/789/refund         # 결제 환불

# 검색 (리소스가 아닌 동사적 행위)
GET /search?q=keyword&type=users

# 배치 작업
POST /users/batch
Body: { "ids": [1, 2, 3], "action": "deactivate" }

# 집계
GET /orders/statistics
GET /dashboard/metrics

3. HTTP 메서드 올바른 사용

3.1 메서드별 특성

메서드목적멱등성안전성요청 Body응답 Body
GET조회YesYesNoYes
POST생성NoNoYesYes
PUT전체 교체YesNoYes선택
PATCH부분 수정No*NoYesYes
DELETE삭제YesNo선택선택
HEAD헤더만 조회YesYesNoNo
OPTIONS지원 메서드 확인YesYesNoYes

*PATCH는 구현에 따라 멱등일 수 있음

3.2 멱등성 (Idempotency) 상세

# PUT - 멱등 (같은 요청을 여러 번 보내도 결과 동일)
PUT /users/123
Body: { "name": "Alice", "email": "alice@example.com" }
# 1번 보내든 10번 보내든 결과는 동일

# DELETE - 멱등
DELETE /users/123
# 첫 번째: 200 OK (삭제됨)
# 두 번째: 404 Not Found (이미 삭제됨) - 부작용 없음

# POST - 비멱등 (같은 요청을 보낼 때마다 새 리소스 생성)
POST /orders
Body: { "product_id": 1, "quantity": 2 }
# 2번 보내면 주문이 2개 생성됨!

멱등성 키로 POST를 안전하게 만들기:

POST /payments
Headers:
  Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
Body: { "amount": 10000, "currency": "KRW" }

# 같은 Idempotency-Key로 재전송하면 중복 결제 방지
# 서버는 키를 저장하고 동일 키 요청시 이전 응답 반환

3.3 PUT vs PATCH

# 원본 데이터
{
  "id": 123,
  "name": "Alice",
  "email": "alice@example.com",
  "role": "user",
  "avatar": "default.png"
}

# PUT: 전체 교체 (누락된 필드는 초기화됨)
PUT /users/123
Body: { "name": "Alice Updated", "email": "alice@new.com" }
# 결과: role과 avatar가 null/기본값으로!

# PATCH: 부분 수정 (보낸 필드만 변경)
PATCH /users/123
Body: { "name": "Alice Updated" }
# 결과: name만 변경, 나머지 필드 유지

4. 상태 코드 (Status Codes)

4.1 주요 상태 코드

2xx 성공:
200 OK                  - 일반 성공 (GET, PUT, PATCH, DELETE)
201 Created             - 리소스 생성 (POST) + Location 헤더
202 Accepted            - 비동기 처리 수락
204 No Content          - 성공, 응답 본문 없음 (DELETE)

3xx 리디렉션:
301 Moved Permanently   - 영구 이동
302 Found               - 임시 이동
304 Not Modified        - 캐시된 버전 사용 가능

4xx 클라이언트 에러:
400 Bad Request         - 잘못된 요청 (유효성 검사 실패)
401 Unauthorized        - 인증 필요 (로그인 안 됨)
403 Forbidden           - 인가 실패 (권한 없음)
404 Not Found           - 리소스 없음
405 Method Not Allowed  - 지원하지 않는 HTTP 메서드
409 Conflict            - 리소스 충돌 (중복 생성 등)
422 Unprocessable Entity - 문법은 맞지만 의미가 잘못됨
429 Too Many Requests   - 레이트 리밋 초과

5xx 서버 에러:
500 Internal Server Error - 서버 내부 오류
502 Bad Gateway          - 업스트림 서버 오류
503 Service Unavailable  - 서비스 일시 중단
504 Gateway Timeout      - 업스트림 서버 타임아웃

4.2 상태 코드 선택 가이드

POST로 리소스 생성 성공 → 201 Created
  Response Headers:
    Location: /users/124
  Response Body:
    { "id": 124, "name": "Bob", ... }

DELETE 성공:
  옵션 1: 204 No Content (본문 없음)
  옵션 2: 200 OK + 삭제된 리소스 정보

인증 vs 인가:
  로그인하지 않음 → 401 Unauthorized
  로그인했지만 권한 없음 → 403 Forbidden

유효성 검사 실패:
  JSON 파싱 실패 → 400 Bad Request
  필드 값이 잘못됨 → 422 Unprocessable Entity

리소스 충돌:
  이메일 중복 → 409 Conflict
  낙관적 락 실패 → 409 Conflict

5. 에러 응답 설계

5.1 RFC 7807 Problem Details

{
  "type": "https://api.example.com/errors/validation-error",
  "title": "Validation Error",
  "status": 422,
  "detail": "One or more fields failed validation.",
  "instance": "/users/123",
  "errors": [
    {
      "field": "email",
      "code": "INVALID_FORMAT",
      "message": "Must be a valid email address"
    },
    {
      "field": "age",
      "code": "OUT_OF_RANGE",
      "message": "Must be between 0 and 150"
    }
  ],
  "timestamp": "2025-03-25T10:30:00Z",
  "trace_id": "abc-123-def-456"
}

5.2 에러 코드 체계

에러 코드 네이밍 컨벤션:

AUTH_001  - 인증 토큰 만료
AUTH_002  - 잘못된 인증 정보
AUTH_003  - 계정 잠김

USER_001  - 사용자를 찾을 수 없음
USER_002  - 이메일 중복
USER_003  - 비밀번호 정책 불일치

ORDER_001 - 재고 부족
ORDER_002 - 결제 실패
ORDER_003 - 이미 취소된 주문

RATE_001  - API 호출 한도 초과
RATE_002  - 일일 한도 초과

5.3 에러 응답 구현 예시

from flask import Flask, jsonify
from werkzeug.exceptions import HTTPException

app = Flask(__name__)

class APIError(Exception):
    def __init__(self, status, error_type, title, detail, errors=None):
        self.status = status
        self.error_type = error_type
        self.title = title
        self.detail = detail
        self.errors = errors or []

@app.errorhandler(APIError)
def handle_api_error(error):
    response = {
        "type": f"https://api.example.com/errors/{error.error_type}",
        "title": error.title,
        "status": error.status,
        "detail": error.detail,
    }
    if error.errors:
        response["errors"] = error.errors
    return jsonify(response), error.status

@app.errorhandler(404)
def not_found(e):
    return jsonify({
        "type": "https://api.example.com/errors/not-found",
        "title": "Resource Not Found",
        "status": 404,
        "detail": "The requested resource does not exist."
    }), 404

# 사용 예시
@app.route('/users', methods=['POST'])
def create_user():
    errors = []
    if not request.json.get('email'):
        errors.append({
            "field": "email",
            "code": "REQUIRED",
            "message": "Email is required"
        })
    if errors:
        raise APIError(
            status=422,
            error_type="validation-error",
            title="Validation Error",
            detail="One or more fields failed validation.",
            errors=errors
        )

6. 페이지네이션 (Pagination)

6.1 Offset 기반

GET /users?page=3&per_page=20

Response:
{
  "data": [...],
  "pagination": {
    "page": 3,
    "per_page": 20,
    "total_items": 1543,
    "total_pages": 78,
    "has_next": true,
    "has_prev": true
  }
}

장점: 구현 간단, 페이지 번호로 직접 이동 가능
단점: 대용량에서 느림 (OFFSET 10000), 데이터 변경시 중복/누락 발생

Offset의 문제:

-- page=500, per_page=20일 때
SELECT * FROM users ORDER BY id LIMIT 20 OFFSET 9980;
-- DB는 9980개를 읽고 버린 후 20개를 반환 -> 매우 비효율적!

6.2 Cursor 기반 (권장)

GET /users?limit=20&cursor=eyJpZCI6MTIzfQ==

Response:
{
  "data": [...],
  "pagination": {
    "next_cursor": "eyJpZCI6MTQzfQ==",
    "prev_cursor": "eyJpZCI6MTI0fQ==",
    "has_next": true,
    "has_prev": true,
    "limit": 20
  }
}

Cursor 구현:

import base64
import json

def encode_cursor(last_item):
    """마지막 항목의 정렬 키를 커서로 인코딩"""
    cursor_data = {"id": last_item["id"], "created_at": last_item["created_at"]}
    return base64.urlsafe_b64encode(
        json.dumps(cursor_data).encode()
    ).decode()

def decode_cursor(cursor_str):
    """커서를 디코딩하여 정렬 키 추출"""
    return json.loads(
        base64.urlsafe_b64decode(cursor_str.encode()).decode()
    )

def get_users(cursor=None, limit=20):
    query = "SELECT * FROM users"
    if cursor:
        decoded = decode_cursor(cursor)
        # WHERE 절로 커서 이후 데이터만 조회
        query += f" WHERE id > {decoded['id']}"
    query += f" ORDER BY id ASC LIMIT {limit + 1}"

    results = db.execute(query)
    has_next = len(results) > limit
    items = results[:limit]

    return {
        "data": items,
        "pagination": {
            "next_cursor": encode_cursor(items[-1]) if has_next else None,
            "has_next": has_next,
            "limit": limit
        }
    }

6.3 Keyset 기반

-- Cursor의 핵심은 Keyset Pagination
-- 복합 정렬 키 사용 예시

-- 첫 페이지
SELECT * FROM orders
ORDER BY created_at DESC, id DESC
LIMIT 20;

-- 다음 페이지 (마지막 항목: created_at='2025-03-20', id=456)
SELECT * FROM orders
WHERE (created_at, id) < ('2025-03-20', 456)
ORDER BY created_at DESC, id DESC
LIMIT 20;

-- 인덱스가 있으면 매우 빠름!
CREATE INDEX idx_orders_cursor ON orders(created_at DESC, id DESC);

6.4 페이지네이션 비교

방식성능일관성특정 페이지 이동구현 복잡도
Offset대용량에서 느림데이터 변경시 문제가능낮음
Cursor일정한 성능일관성 유지불가중간
Keyset매우 빠름일관성 유지불가중간

7. 필터링, 정렬, 필드 선택

7.1 필터링

# 기본 필터링
GET /products?category=electronics&status=available

# 범위 필터
GET /products?min_price=100&max_price=500
GET /orders?created_after=2025-01-01&created_before=2025-03-25

# 다중 값 필터
GET /products?tags=phone,tablet,laptop

# 검색
GET /products?q=wireless+keyboard

# 복합 필터 (고급)
GET /products?filter[category]=electronics&filter[price][gte]=100&filter[price][lte]=500

7.2 정렬

# 단일 필드 정렬
GET /users?sort=name          # 오름차순
GET /users?sort=-name         # 내림차순

# 다중 필드 정렬
GET /users?sort=-created_at,name

# JSON:API 스타일
GET /articles?sort=-published_at,title

7.3 필드 선택 (Sparse Fieldsets)

# 필요한 필드만 요청 (대역폭 절약)
GET /users/123?fields=id,name,email

# 관계 리소스 포함 (임베딩)
GET /users/123?include=orders,profile
GET /articles/456?include=author,comments&fields[articles]=title,body&fields[author]=name

# GraphQL 스타일 vs REST
# REST: /users/123?fields=name,email&include=orders
# GraphQL: query { user(id: 123) { name email orders { id total } } }

8. 버저닝 (Versioning)

8.1 URL 경로 버저닝 (가장 일반적)

GET /v1/users/123
GET /v2/users/123

장점:
- 명확하고 직관적
- 브라우저에서 테스트 쉬움
- 라우팅 분리 용이

단점:
- URL이 리소스 위치를 나타내야 한다는 REST 원칙 위반
- 버전 변경시 모든 URL 변경 필요

8.2 헤더 버저닝

# Accept 헤더 (Content Negotiation)
GET /users/123
Accept: application/vnd.myapi.v2+json

# 커스텀 헤더
GET /users/123
X-API-Version: 2

장점: URL 깔끔, REST 원칙에 부합
단점: 테스트 불편, 디버깅 어려움

8.3 버전 관리 전략

1. 호환성 유지 원칙:
   - 필드 추가는 비파괴적 (기존 클라이언트 영향 없음)
   - 필드 제거/이름 변경은 파괴적 → 새 버전 필요
   - 필드 타입 변경은 파괴적 → 새 버전 필요

2. Deprecation 프로세스:
   Phase 1: 새 버전 출시 + 기존 버전에 Sunset 헤더 추가
   Phase 2: 기존 버전에 경고 응답 추가
   Phase 3: 기존 버전 종료 (최소 6개월 유예)

   HTTP Headers:
   Sunset: Sat, 01 Jan 2026 00:00:00 GMT
   Deprecation: true
   Link: <https://api.example.com/v2>; rel="successor-version"

3. API 변경 로그:
   - 모든 변경사항 문서화
   - 마이그레이션 가이드 제공
   - 클라이언트에 사전 알림

9. 인증과 보안 (Authentication and Security)

9.1 인증 방식 비교

방식사용 사례장점단점
API Key서버 간 통신, 단순 API구현 간단세분화된 권한 어려움
OAuth 2.0사용자 대신 작업표준화, 범위 제어구현 복잡
JWT무상태 인증서버 확장 용이토큰 폐기 어려움
Session전통적 웹앱서버 제어 쉬움확장성 제한

9.2 JWT (JSON Web Token)

JWT 구조:
Header.Payload.Signature

Header:
{
  "alg": "RS256",
  "typ": "JWT"
}

Payload:
{
  "sub": "user-123",
  "email": "alice@example.com",
  "roles": ["admin", "user"],
  "iat": 1711353000,
  "exp": 1711356600
}

Signature:
RS256(base64(header) + "." + base64(payload), privateKey)

JWT 사용 패턴:

# Access Token + Refresh Token
Access Token: 15분 유효
Refresh Token: 7일 유효

1. 로그인 → Access + Refresh 토큰 발급
2. API 호출 → Authorization: Bearer {access_token}
3. Access Token 만료 → Refresh Token으로 갱신
4. Refresh Token 만료 → 재로그인

# 헤더 예시
GET /users/me
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...

9.3 OAuth 2.0 플로우

Authorization Code Flow (웹 앱):
1. 사용자 → 인가 서버로 리디렉트
2. 사용자 로그인 + 권한 동의
3. 인가 코드를 콜백 URL로 반환
4. 인가 코드로 Access Token 교환
5. Access Token으로 API 호출

Client Credentials Flow (서버 간):
1. 클라이언트 ID + Secret으로 직접 토큰 요청
2. Access Token 발급
3. API 호출

9.4 API 보안 체크리스트

1. HTTPS 필수 (HTTP 절대 금지)
2. 인증 토큰은 헤더로만 전송 (URL 파라미터 금지)
3. 입력 유효성 검사 (SQL Injection, XSS 방지)
4. CORS 설정 (허용 도메인만)
5. Rate Limiting 적용
6. 민감 데이터 로깅 금지
7. 에러 메시지에 내부 정보 노출 금지
8. API Key는 환경 변수로 관리
9. 정기적인 보안 감사
10. 의존성 취약점 스캔

10. 레이트 리미팅 (Rate Limiting)

10.1 응답 헤더

HTTP/1.1 200 OK
X-RateLimit-Limit: 1000          # 시간당 최대 요청 수
X-RateLimit-Remaining: 742       # 남은 요청 수
X-RateLimit-Reset: 1711360000    # 리셋 시간 (Unix timestamp)

# 한도 초과시
HTTP/1.1 429 Too Many Requests
Retry-After: 60                  # 60초 후 재시도
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1711360000

{
  "type": "https://api.example.com/errors/rate-limit-exceeded",
  "title": "Rate Limit Exceeded",
  "status": 429,
  "detail": "You have exceeded the rate limit of 1000 requests per hour.",
  "retry_after": 60
}

10.2 레이트 리미팅 전략

1. 사용자별 제한:
   - 인증된 사용자: 1000 req/hour
   - 미인증 사용자: 100 req/hour

2. 엔드포인트별 제한:
   - GET /users: 100 req/min
   - POST /users: 10 req/min
   - POST /payments: 5 req/min

3. 티어별 제한:
   - Free: 100 req/day
   - Basic: 10,000 req/day
   - Pro: 100,000 req/day
   - Enterprise: Custom

4. 구현 (Redis):
   key = "rate:user:123:2025-03-25-10"  # 사용자 + 시간 윈도우
   INCR key
   EXPIRE key 3600

11. HATEOAS

11.1 HATEOAS란?

{
  "id": 123,
  "name": "Alice",
  "email": "alice@example.com",
  "status": "active",
  "_links": {
    "self": {
      "href": "/users/123"
    },
    "orders": {
      "href": "/users/123/orders"
    },
    "deactivate": {
      "href": "/users/123/deactivate",
      "method": "POST"
    },
    "update": {
      "href": "/users/123",
      "method": "PUT"
    }
  }
}

11.2 HATEOAS 장단점

장점:
- API 탐색 가능 (self-documenting)
- 클라이언트가 URL 하드코딩 불필요
- 서버가 URL 구조 변경 가능
- 가능한 동작을 응답에서 발견

단점:
- 응답 크기 증가
- 구현 복잡도 증가
- 대부분의 클라이언트가 활용하지 않음
- 실무에서 필수가 아님

결론: 공개 API에서는 권장, 내부 API에서는 선택적

12. 벌크 작업과 비동기 처리

12.1 배치 엔드포인트

// POST /users/batch
{
  "operations": [
    { "method": "POST", "body": { "name": "Alice", "email": "a@ex.com" } },
    { "method": "POST", "body": { "name": "Bob", "email": "b@ex.com" } },
    { "method": "POST", "body": { "name": "Carol", "email": "c@ex.com" } }
  ]
}

// Response: 207 Multi-Status
{
  "results": [
    { "status": 201, "data": { "id": 1, "name": "Alice" } },
    { "status": 201, "data": { "id": 2, "name": "Bob" } },
    { "status": 409, "error": { "code": "USER_002", "message": "Email already exists" } }
  ]
}

12.2 비동기 처리 패턴

# 장시간 작업 (보고서 생성, 대량 데이터 처리)

POST /reports
Body: { "type": "annual", "year": 2025 }

Response: 202 Accepted
{
  "task_id": "task-abc-123",
  "status": "processing",
  "estimated_time": 120,
  "_links": {
    "status": { "href": "/tasks/task-abc-123" },
    "cancel": { "href": "/tasks/task-abc-123/cancel", "method": "POST" }
  }
}

# 상태 확인
GET /tasks/task-abc-123

{
  "task_id": "task-abc-123",
  "status": "completed",
  "result": {
    "download_url": "/reports/2025-annual.pdf"
  },
  "completed_at": "2025-03-25T10:35:00Z"
}

13. OpenAPI 3.1 스펙

13.1 OpenAPI 기본 구조

openapi: "3.1.0"
info:
  title: User Management API
  description: API for managing users
  version: "1.0.0"
  contact:
    email: api@example.com

servers:
  - url: https://api.example.com/v1
    description: Production
  - url: https://staging-api.example.com/v1
    description: Staging

paths:
  /users:
    get:
      summary: List users
      operationId: listUsers
      tags:
        - Users
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
            maximum: 100
        - name: cursor
          in: query
          schema:
            type: string
      responses:
        "200":
          description: Successful response
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/User"
                  pagination:
                    $ref: "#/components/schemas/Pagination"

    post:
      summary: Create user
      operationId: createUser
      tags:
        - Users
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateUserRequest"
      responses:
        "201":
          description: User created
          headers:
            Location:
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/User"
        "422":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"

components:
  schemas:
    User:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string
        email:
          type: string
          format: email
      required:
        - id
        - name
        - email

    CreateUserRequest:
      type: object
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 100
        email:
          type: string
          format: email
      required:
        - name
        - email

13.2 OpenAPI 코드 생성

# OpenAPI에서 코드 자동 생성

1. 클라이언트 SDK 생성:
   npx openapi-generator-cli generate \
     -i openapi.yaml \
     -g typescript-fetch \
     -o ./generated-client

2. 서버 스텁 생성:
   npx openapi-generator-cli generate \
     -i openapi.yaml \
     -g python-flask \
     -o ./generated-server

3. 모의 서버:
   npx prism mock openapi.yaml

4. API 문서:
   npx redocly build-docs openapi.yaml

14. API 설계 안티패턴

14.1 10가지 흔한 실수

1. 동사를 URL에 사용
   Bad:  POST /createUser
   Good: POST /users

2. 일관성 없는 네이밍
   Bad:  /users, /getOrders, /product-list
   Good: /users, /orders, /products

3. 단수형 리소스명
   Bad:  /user/123
   Good: /users/123

4. HTTP 메서드 무시
   Bad:  POST /users/123/delete
   Good: DELETE /users/123

5. 항상 200 반환
   Bad:  200 OK { "error": true, "message": "Not found" }
   Good: 404 Not Found { "title": "Not Found", ... }

6. 중첩 과다
   Bad:  /countries/kr/cities/seoul/districts/gangnam/restaurants
   Good: /restaurants?city=seoul&district=gangnam

7. 버전 없는 API
   Bad:  /users (언제든 바뀔 수 있음)
   Good: /v1/users

8. 에러 응답 표준 없음
   Bad:  { "error": "something went wrong" }
   Good: RFC 7807 Problem Details 형식

9. 페이지네이션 없음
   Bad:  GET /logs (100만 건 전체 반환)
   Good: GET /logs?limit=50&cursor=abc

10. 보안 헤더 누락
    Bad:  HTTP 허용, CORS 전체 허용
    Good: HTTPS 필수, 최소 CORS, 적절한 헤더

15. GraphQL vs REST vs gRPC 비교

┌─────────────┬──────────────────┬──────────────────┬──────────────────┐
│   특성       │     RESTGraphQL       │     gRPC         │
├─────────────┼──────────────────┼──────────────────┼──────────────────┤
│ 프로토콜     │ HTTP/1.1, 2HTTP/1.1, 2HTTP/2│ 데이터 형식  │ JSON/XMLJSONProtocol Buffers│ 스키마       │ OpenAPI (선택)SDL (필수).proto (필수)│ 오버/언더    │ 오버페칭 가능    │ 클라이언트 결정  │ 정확한 스키마    │
│   페칭       │                  │                  │                  │
│ 실시간       │ WebSocket/SSESubscriptionsBidirectional│              │                  │                  │ Streaming│ 사용 사례    │ 공개 API,        │ 복잡한 데이터    │ 마이크로서비스   │
│              │ 단순 CRUD        │ 관계, 모바일     │ 내부 통신        │
│ 학습 곡선    │ 낮음             │ 중간             │ 높음             │
│ 캐싱         │ HTTP 캐싱 용이   │ 어려움           │ 없음             │
│ 에러 처리    │ HTTP 상태 코드   │ errors 배열      │ Status codes     │
└─────────────┴──────────────────┴──────────────────┴──────────────────┘

선택 가이드:

REST 선택:
- 공개 API (외부 개발자 대상)
- 간단한 CRUD 작업
- HTTP 캐싱 필요
- 브라우저에서 직접 호출

GraphQL 선택:
- 다양한 클라이언트 (, 모바일, IoT)
- 복잡한 데이터 관계
- 오버페칭/언더페칭 문제 해결 필요
- 빠른 프론트엔드 개발

gRPC 선택:
- 마이크로서비스 간 내부 통신
- 낮은 지연 시간 요구
- 양방향 스트리밍 필요
- 강타입 계약 필요

16. 퀴즈

Q1. REST 원칙

REST의 6가지 제약 조건 중 "Stateless"가 확장성에 기여하는 이유를 설명하세요.

답변: Stateless 제약은 서버가 클라이언트의 상태를 저장하지 않으므로, 각 요청이 처리에 필요한 모든 정보를 포함합니다. 이 덕분에:

  1. 로드 밸런싱이 단순: 어떤 서버든 동일하게 요청 처리 가능 (세션 고정 불필요)
  2. 수평 확장 용이: 서버 추가/제거가 자유로움
  3. 장애 복구 쉬움: 서버 장애시 다른 서버가 즉시 대체 가능
  4. 캐싱 효율 향상: 요청이 독립적이므로 캐싱 키 생성이 명확

Q2. 페이지네이션

Offset 페이지네이션 대신 Cursor 페이지네이션을 선택해야 하는 상황 2가지를 설명하세요.

답변:

  1. 대용량 데이터셋: 수백만 건의 데이터에서 Offset은 OFFSET 100000처럼 많은 행을 건너뛰어야 하므로 매우 느립니다. Cursor는 인덱스를 활용하여 WHERE 절로 직접 이동하므로 데이터 양에 관계없이 일정한 성능을 보입니다.

  2. 실시간 데이터: 데이터가 빈번하게 추가/삭제되는 경우, Offset은 같은 페이지를 다시 요청할 때 중복이나 누락이 발생합니다. Cursor는 마지막으로 본 항목의 고유 식별자를 기준으로 하므로 일관성을 유지합니다.

Q3. 에러 처리

401 Unauthorized와 403 Forbidden의 차이를 구체적 예시로 설명하세요.

답변:

  • 401 Unauthorized (인증 실패): 클라이언트가 자신이 누구인지 증명하지 못한 상태입니다. 예를 들어, Authorization 헤더 없이 API를 호출하거나, 만료된 JWT 토큰을 사용한 경우입니다. "당신이 누구인지 모르겠습니다. 로그인하세요."

  • 403 Forbidden (인가 실패): 클라이언트가 인증은 되었지만, 해당 리소스에 접근할 권한이 없는 상태입니다. 예를 들어, 일반 사용자가 관리자 전용 API를 호출한 경우입니다. "당신이 누구인지 알지만, 이 리소스에 접근할 권한이 없습니다."

Q4. 버저닝

URL 경로 버저닝(/v1/users)과 Accept 헤더 버저닝의 장단점을 비교하세요.

답변: URL 경로 버저닝 (/v1/users):

  • 장점: 직관적, 브라우저에서 테스트 쉬움, 라우팅 분리 용이, 캐싱 쉬움
  • 단점: REST 원칙 위반 (URL은 리소스 위치), 모든 URL 변경 필요

Accept 헤더 버저닝 (Accept: application/vnd.myapi.v2+json):

  • 장점: URL 깔끔, REST 원칙 부합, 하나의 URL로 여러 버전
  • 단점: 테스트 불편 (curl/Postman 필요), 디버깅 어려움, 캐싱 복잡

실무에서는 URL 경로 버저닝이 압도적으로 많이 사용됩니다 (GitHub, Stripe, Google 등).

Q5. 보안

JWT의 장단점과 토큰 탈취시 대응 방법을 설명하세요.

답변: 장점: 서버 세션 저장 불필요 (Stateless), 수평 확장 용이, 마이크로서비스 간 전파 용이, 클레임에 사용자 정보 포함

단점: 토큰 폐기 어려움 (만료 전까지 유효), 토큰 크기가 세션 ID보다 큼, Payload 암호화 안 됨 (인코딩만)

토큰 탈취 대응:

  1. Access Token 유효 시간 짧게 설정 (15분)
  2. Refresh Token 회전 (사용시 새 토큰 발급, 기존 무효화)
  3. 토큰 블랙리스트 (Redis에 폐기된 토큰 저장)
  4. IP/User-Agent 바인딩
  5. 의심 활동 감지시 모든 Refresh Token 무효화

참고 자료

  1. RFC 7231 - HTTP/1.1 Semantics and Content - https://tools.ietf.org/html/rfc7231
  2. RFC 7807 - Problem Details for HTTP APIs - https://tools.ietf.org/html/rfc7807
  3. OpenAPI Specification 3.1 - https://spec.openapis.org/oas/v3.1.0
  4. JSON:API Specification - https://jsonapi.org/
  5. Google API Design Guide - https://cloud.google.com/apis/design
  6. Microsoft REST API Guidelines - https://github.com/microsoft/api-guidelines
  7. Stripe API Reference - https://stripe.com/docs/api
  8. GitHub REST API - https://docs.github.com/en/rest
  9. Zalando RESTful API Guidelines - https://opensource.zalando.com/restful-api-guidelines/
  10. REST API Design Rulebook - Mark Masse (O'Reilly)
  11. OAuth 2.0 RFC 6749 - https://tools.ietf.org/html/rfc6749
  12. JWT RFC 7519 - https://tools.ietf.org/html/rfc7519
  13. Roy Fielding's Dissertation - https://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm
  14. Richardson Maturity Model - https://martinfowler.com/articles/richardsonMaturityModel.html

REST API Design Best Practices 2025: Naming, Versioning, Error Handling, Pagination, Security

Introduction

REST APIs are the foundation of modern web services. Almost every piece of software -- mobile apps, SPAs, microservices, IoT devices -- communicates through REST APIs. Yet a surprising number of APIs claiming to be "RESTful" actually violate REST principles.

Well-designed APIs improve developer experience (DX), simplify maintenance, and ensure scalability and security. Poorly designed APIs increase communication costs between teams, create security vulnerabilities, and accumulate technical debt.

This guide covers all REST API design best practices, from resource naming to versioning, error handling, authentication, and OpenAPI.


1. REST Principles

1.1 The 6 REST Constraints

1. Client-Server (Separation of Concerns)
   - Separate UI and data storage concerns
   - Enable independent evolution

2. Stateless
   - Each request contains complete information
   - Server does not store client state
   - Improves scalability

3. Cacheable
   - Responses must declare cacheability
   - Use Cache-Control, ETag, Last-Modified headers

4. Uniform Interface
   - Resource identification (URI)
   - Resource manipulation through representations
   - Self-descriptive messages
   - HATEOAS

5. Layered System
   - Client cannot tell if communicating directly with server or intermediary
   - Load balancers, caches, gateways can be added

6. Code-on-Demand (Optional)
   - Server can send executable code to client
   - The only optional constraint

1.2 Richardson Maturity Model

Level 3: Hypermedia Controls (HATEOAS)
  --> Responses include links to related resources
Level 2: HTTP Methods
  --> Correct use of GET, POST, PUT, DELETE
Level 1: Resources
  --> Individual URIs for resources
Level 0: The Swamp of POX
  --> Single endpoint, all operations via POST

Most APIs target Level 2.
Level 3 (HATEOAS) is ideal but optional in practice.

2. Resource Naming

2.1 Basic Rules

Good:
GET    /users               # List users
GET    /users/123            # Get specific user
POST   /users               # Create user
PUT    /users/123            # Full update user
PATCH  /users/123            # Partial update user
DELETE /users/123            # Delete user

Bad:
GET    /getUsers             # No verbs
GET    /user/123             # Use plural
POST   /createUser           # No verbs
POST   /user/123/delete      # Use HTTP methods

2.2 Hierarchical Relationships

# User's orders list
GET /users/123/orders

# User's specific order
GET /users/123/orders/456

# Order's items list
GET /users/123/orders/456/items

# Warning: Avoid nesting beyond 3 levels
# Bad:
GET /users/123/orders/456/items/789/reviews

# Good (alternatives):
GET /items/789/reviews
GET /reviews?item_id=789

2.3 Naming Conventions

# Use lowercase + hyphens (kebab-case)
Good: /user-profiles
Bad:  /userProfiles, /user_profiles, /UserProfiles

# No file extensions
Good: Accept: application/json
Bad:  /users/123.json

# No trailing slashes
Good: /users
Bad:  /users/

# Use query parameters for filtering
GET /users?status=active&role=admin
GET /products?category=electronics&min_price=100&max_price=500

# Sorting
GET /users?sort=created_at&order=desc
GET /users?sort=-created_at,+name  # - descending, + ascending

# Field Selection (Sparse Fieldsets)
GET /users/123?fields=name,email,avatar

2.4 Handling Non-Resource Operations

# Actions: express as sub-resources
POST /users/123/activate          # Activate user
POST /users/123/deactivate        # Deactivate user
POST /orders/456/cancel           # Cancel order
POST /payments/789/refund         # Refund payment

# Search (verb-like action, not a resource)
GET /search?q=keyword&type=users

# Batch operations
POST /users/batch
Body: { "ids": [1, 2, 3], "action": "deactivate" }

# Aggregations
GET /orders/statistics
GET /dashboard/metrics

3. Correct HTTP Method Usage

3.1 Method Characteristics

MethodPurposeIdempotentSafeRequest BodyResponse Body
GETReadYesYesNoYes
POSTCreateNoNoYesYes
PUTFull replaceYesNoYesOptional
PATCHPartial updateNo*NoYesYes
DELETEDeleteYesNoOptionalOptional
HEADHeaders onlyYesYesNoNo
OPTIONSAvailable methodsYesYesNoYes

*PATCH can be idempotent depending on implementation

3.2 Idempotency in Detail

# PUT - Idempotent (same request multiple times = same result)
PUT /users/123
Body: { "name": "Alice", "email": "alice@example.com" }
# Sending once or 10 times yields same result

# DELETE - Idempotent
DELETE /users/123
# First time: 200 OK (deleted)
# Second time: 404 Not Found (already deleted) - no side effects

# POST - NOT idempotent (each request creates new resource)
POST /orders
Body: { "product_id": 1, "quantity": 2 }
# Sending twice creates 2 orders!

Making POST safe with idempotency keys:

POST /payments
Headers:
  Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
Body: { "amount": 10000, "currency": "USD" }

# Resending with same Idempotency-Key prevents duplicate payments
# Server stores the key and returns previous response for same key

3.3 PUT vs PATCH

# Original data
{
  "id": 123,
  "name": "Alice",
  "email": "alice@example.com",
  "role": "user",
  "avatar": "default.png"
}

# PUT: Full replacement (missing fields reset)
PUT /users/123
Body: { "name": "Alice Updated", "email": "alice@new.com" }
# Result: role and avatar become null/default!

# PATCH: Partial update (only sent fields change)
PATCH /users/123
Body: { "name": "Alice Updated" }
# Result: only name changes, other fields preserved

4. Status Codes

4.1 Key Status Codes

2xx Success:
200 OK                  - General success (GET, PUT, PATCH, DELETE)
201 Created             - Resource created (POST) + Location header
202 Accepted            - Async processing accepted
204 No Content          - Success with no response body (DELETE)

3xx Redirection:
301 Moved Permanently   - Permanent redirect
302 Found               - Temporary redirect
304 Not Modified        - Use cached version

4xx Client Errors:
400 Bad Request         - Invalid request (validation failure)
401 Unauthorized        - Authentication required (not logged in)
403 Forbidden           - Authorization failure (no permission)
404 Not Found           - Resource does not exist
405 Method Not Allowed  - Unsupported HTTP method
409 Conflict            - Resource conflict (duplicate creation)
422 Unprocessable Entity - Syntax ok but semantics wrong
429 Too Many Requests   - Rate limit exceeded

5xx Server Errors:
500 Internal Server Error - Server error
502 Bad Gateway          - Upstream server error
503 Service Unavailable  - Service temporarily down
504 Gateway Timeout      - Upstream server timeout

4.2 Status Code Selection Guide

POST resource creation success -> 201 Created
  Response Headers:
    Location: /users/124
  Response Body:
    { "id": 124, "name": "Bob", ... }

DELETE success:
  Option 1: 204 No Content (no body)
  Option 2: 200 OK + deleted resource info

Authentication vs Authorization:
  Not logged in -> 401 Unauthorized
  Logged in but no permission -> 403 Forbidden

Validation failures:
  JSON parse failure -> 400 Bad Request
  Field value invalid -> 422 Unprocessable Entity

Resource conflicts:
  Duplicate email -> 409 Conflict
  Optimistic lock failure -> 409 Conflict

5. Error Response Design

5.1 RFC 7807 Problem Details

{
  "type": "https://api.example.com/errors/validation-error",
  "title": "Validation Error",
  "status": 422,
  "detail": "One or more fields failed validation.",
  "instance": "/users/123",
  "errors": [
    {
      "field": "email",
      "code": "INVALID_FORMAT",
      "message": "Must be a valid email address"
    },
    {
      "field": "age",
      "code": "OUT_OF_RANGE",
      "message": "Must be between 0 and 150"
    }
  ],
  "timestamp": "2025-03-25T10:30:00Z",
  "trace_id": "abc-123-def-456"
}

5.2 Error Code System

Error code naming convention:

AUTH_001  - Authentication token expired
AUTH_002  - Invalid credentials
AUTH_003  - Account locked

USER_001  - User not found
USER_002  - Email already exists
USER_003  - Password policy violation

ORDER_001 - Insufficient stock
ORDER_002 - Payment failed
ORDER_003 - Order already cancelled

RATE_001  - API call limit exceeded
RATE_002  - Daily limit exceeded

5.3 Error Response Implementation

from flask import Flask, jsonify
from werkzeug.exceptions import HTTPException

app = Flask(__name__)

class APIError(Exception):
    def __init__(self, status, error_type, title, detail, errors=None):
        self.status = status
        self.error_type = error_type
        self.title = title
        self.detail = detail
        self.errors = errors or []

@app.errorhandler(APIError)
def handle_api_error(error):
    response = {
        "type": f"https://api.example.com/errors/{error.error_type}",
        "title": error.title,
        "status": error.status,
        "detail": error.detail,
    }
    if error.errors:
        response["errors"] = error.errors
    return jsonify(response), error.status

@app.errorhandler(404)
def not_found(e):
    return jsonify({
        "type": "https://api.example.com/errors/not-found",
        "title": "Resource Not Found",
        "status": 404,
        "detail": "The requested resource does not exist."
    }), 404

6. Pagination

6.1 Offset-based

GET /users?page=3&per_page=20

Response:
{
  "data": [...],
  "pagination": {
    "page": 3,
    "per_page": 20,
    "total_items": 1543,
    "total_pages": 78,
    "has_next": true,
    "has_prev": true
  }
}

Pros: Simple implementation, direct page jump possible
Cons: Slow on large datasets (OFFSET 10000), duplicates/misses on data changes

The Offset Problem:

-- When page=500, per_page=20
SELECT * FROM users ORDER BY id LIMIT 20 OFFSET 9980;
-- DB reads 9980 rows and discards them, returns 20 -> very inefficient!
GET /users?limit=20&cursor=eyJpZCI6MTIzfQ==

Response:
{
  "data": [...],
  "pagination": {
    "next_cursor": "eyJpZCI6MTQzfQ==",
    "prev_cursor": "eyJpZCI6MTI0fQ==",
    "has_next": true,
    "has_prev": true,
    "limit": 20
  }
}

Cursor Implementation:

import base64
import json

def encode_cursor(last_item):
    """Encode sort key of last item as cursor"""
    cursor_data = {"id": last_item["id"], "created_at": last_item["created_at"]}
    return base64.urlsafe_b64encode(
        json.dumps(cursor_data).encode()
    ).decode()

def decode_cursor(cursor_str):
    """Decode cursor to extract sort key"""
    return json.loads(
        base64.urlsafe_b64decode(cursor_str.encode()).decode()
    )

def get_users(cursor=None, limit=20):
    query = "SELECT * FROM users"
    if cursor:
        decoded = decode_cursor(cursor)
        query += f" WHERE id > {decoded['id']}"
    query += f" ORDER BY id ASC LIMIT {limit + 1}"

    results = db.execute(query)
    has_next = len(results) > limit
    items = results[:limit]

    return {
        "data": items,
        "pagination": {
            "next_cursor": encode_cursor(items[-1]) if has_next else None,
            "has_next": has_next,
            "limit": limit
        }
    }

6.3 Keyset-based

-- Keyset Pagination is the core of Cursor
-- Composite sort key example

-- First page
SELECT * FROM orders
ORDER BY created_at DESC, id DESC
LIMIT 20;

-- Next page (last item: created_at='2025-03-20', id=456)
SELECT * FROM orders
WHERE (created_at, id) < ('2025-03-20', 456)
ORDER BY created_at DESC, id DESC
LIMIT 20;

-- Very fast with proper index!
CREATE INDEX idx_orders_cursor ON orders(created_at DESC, id DESC);

6.4 Pagination Comparison

MethodPerformanceConsistencyDirect Page JumpComplexity
OffsetSlow on large dataIssues on data changesYesLow
CursorConsistent performanceMaintains consistencyNoMedium
KeysetVery fastMaintains consistencyNoMedium

7. Filtering, Sorting, Field Selection

7.1 Filtering

# Basic filtering
GET /products?category=electronics&status=available

# Range filters
GET /products?min_price=100&max_price=500
GET /orders?created_after=2025-01-01&created_before=2025-03-25

# Multiple value filter
GET /products?tags=phone,tablet,laptop

# Search
GET /products?q=wireless+keyboard

# Complex filters (advanced)
GET /products?filter[category]=electronics&filter[price][gte]=100&filter[price][lte]=500

7.2 Sorting

# Single field sorting
GET /users?sort=name          # ascending
GET /users?sort=-name         # descending

# Multiple field sorting
GET /users?sort=-created_at,name

# JSON:API style
GET /articles?sort=-published_at,title

7.3 Field Selection (Sparse Fieldsets)

# Request only needed fields (save bandwidth)
GET /users/123?fields=id,name,email

# Include related resources (embedding)
GET /users/123?include=orders,profile
GET /articles/456?include=author,comments&fields[articles]=title,body&fields[author]=name

8. Versioning

8.1 URL Path Versioning (Most Common)

GET /v1/users/123
GET /v2/users/123

Pros:
- Clear and intuitive
- Easy to test in browser
- Easy routing separation

Cons:
- Violates REST principle that URL should identify resource location
- All URLs change on version change

8.2 Header Versioning

# Accept header (Content Negotiation)
GET /users/123
Accept: application/vnd.myapi.v2+json

# Custom header
GET /users/123
X-API-Version: 2

Pros: Clean URLs, follows REST principles
Cons: Testing inconvenient, harder to debug

8.3 Version Management Strategy

1. Compatibility principles:
   - Adding fields is non-breaking (no impact on existing clients)
   - Removing/renaming fields is breaking -> new version needed
   - Changing field types is breaking -> new version needed

2. Deprecation process:
   Phase 1: Release new version + add Sunset header to old
   Phase 2: Add warning responses to old version
   Phase 3: End old version (minimum 6-month grace period)

   HTTP Headers:
   Sunset: Sat, 01 Jan 2026 00:00:00 GMT
   Deprecation: true
   Link: <https://api.example.com/v2>; rel="successor-version"

3. API changelog:
   - Document all changes
   - Provide migration guides
   - Pre-notify clients

9. Authentication and Security

9.1 Authentication Method Comparison

MethodUse CaseProsCons
API KeyServer-to-server, simple APISimple implementationHard to scope permissions
OAuth 2.0Acting on behalf of userStandardized, scope controlComplex implementation
JWTStateless authenticationEasy server scalingHard to revoke tokens
SessionTraditional web appsEasy server controlScalability limited

9.2 JWT (JSON Web Token)

JWT Structure:
Header.Payload.Signature

Header:
{
  "alg": "RS256",
  "typ": "JWT"
}

Payload:
{
  "sub": "user-123",
  "email": "alice@example.com",
  "roles": ["admin", "user"],
  "iat": 1711353000,
  "exp": 1711356600
}

Signature:
RS256(base64(header) + "." + base64(payload), privateKey)

JWT Usage Pattern:

# Access Token + Refresh Token
Access Token: 15-minute validity
Refresh Token: 7-day validity

1. Login -> Issue Access + Refresh tokens
2. API call -> Authorization: Bearer access_token_value
3. Access Token expired -> Refresh using Refresh Token
4. Refresh Token expired -> Re-login

# Header example
GET /users/me
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...

9.3 OAuth 2.0 Flows

Authorization Code Flow (Web apps):
1. User -> redirect to authorization server
2. User login + consent
3. Authorization code returned to callback URL
4. Exchange authorization code for Access Token
5. Call API with Access Token

Client Credentials Flow (Server-to-server):
1. Request token directly with Client ID + Secret
2. Access Token issued
3. Call API

9.4 API Security Checklist

1. HTTPS mandatory (never HTTP)
2. Auth tokens in headers only (never URL parameters)
3. Input validation (SQL Injection, XSS prevention)
4. CORS configuration (allowed domains only)
5. Rate Limiting applied
6. Never log sensitive data
7. Never expose internal info in error messages
8. Manage API keys via environment variables
9. Regular security audits
10. Dependency vulnerability scanning

10. Rate Limiting

10.1 Response Headers

HTTP/1.1 200 OK
X-RateLimit-Limit: 1000          # Max requests per hour
X-RateLimit-Remaining: 742       # Remaining requests
X-RateLimit-Reset: 1711360000    # Reset time (Unix timestamp)

# When limit exceeded
HTTP/1.1 429 Too Many Requests
Retry-After: 60                  # Retry after 60 seconds
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1711360000

{
  "type": "https://api.example.com/errors/rate-limit-exceeded",
  "title": "Rate Limit Exceeded",
  "status": 429,
  "detail": "You have exceeded the rate limit of 1000 requests per hour.",
  "retry_after": 60
}

10.2 Rate Limiting Strategies

1. Per-user limits:
   - Authenticated users: 1000 req/hour
   - Unauthenticated users: 100 req/hour

2. Per-endpoint limits:
   - GET /users: 100 req/min
   - POST /users: 10 req/min
   - POST /payments: 5 req/min

3. Tier-based limits:
   - Free: 100 req/day
   - Basic: 10,000 req/day
   - Pro: 100,000 req/day
   - Enterprise: Custom

4. Implementation (Redis):
   key = "rate:user:123:2025-03-25-10"  # user + time window
   INCR key
   EXPIRE key 3600

11. HATEOAS

11.1 What is HATEOAS?

{
  "id": 123,
  "name": "Alice",
  "email": "alice@example.com",
  "status": "active",
  "_links": {
    "self": {
      "href": "/users/123"
    },
    "orders": {
      "href": "/users/123/orders"
    },
    "deactivate": {
      "href": "/users/123/deactivate",
      "method": "POST"
    },
    "update": {
      "href": "/users/123",
      "method": "PUT"
    }
  }
}

11.2 HATEOAS Pros and Cons

Pros:
- API is explorable (self-documenting)
- Client does not need to hardcode URLs
- Server can change URL structure
- Discover available actions from responses

Cons:
- Increased response size
- Implementation complexity
- Most clients do not utilize it
- Not mandatory in practice

Conclusion: Recommended for public APIs, optional for internal APIs

12. Bulk Operations and Async Processing

12.1 Batch Endpoints

// POST /users/batch
{
  "operations": [
    { "method": "POST", "body": { "name": "Alice", "email": "a@ex.com" } },
    { "method": "POST", "body": { "name": "Bob", "email": "b@ex.com" } },
    { "method": "POST", "body": { "name": "Carol", "email": "c@ex.com" } }
  ]
}

// Response: 207 Multi-Status
{
  "results": [
    { "status": 201, "data": { "id": 1, "name": "Alice" } },
    { "status": 201, "data": { "id": 2, "name": "Bob" } },
    { "status": 409, "error": { "code": "USER_002", "message": "Email already exists" } }
  ]
}

12.2 Async Processing Pattern

# Long-running tasks (report generation, bulk data processing)

POST /reports
Body: { "type": "annual", "year": 2025 }

Response: 202 Accepted
{
  "task_id": "task-abc-123",
  "status": "processing",
  "estimated_time": 120,
  "_links": {
    "status": { "href": "/tasks/task-abc-123" },
    "cancel": { "href": "/tasks/task-abc-123/cancel", "method": "POST" }
  }
}

# Check status
GET /tasks/task-abc-123

{
  "task_id": "task-abc-123",
  "status": "completed",
  "result": {
    "download_url": "/reports/2025-annual.pdf"
  },
  "completed_at": "2025-03-25T10:35:00Z"
}

13. OpenAPI 3.1 Specification

13.1 OpenAPI Basic Structure

openapi: "3.1.0"
info:
  title: User Management API
  description: API for managing users
  version: "1.0.0"
  contact:
    email: api@example.com

servers:
  - url: https://api.example.com/v1
    description: Production
  - url: https://staging-api.example.com/v1
    description: Staging

paths:
  /users:
    get:
      summary: List users
      operationId: listUsers
      tags:
        - Users
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
            maximum: 100
        - name: cursor
          in: query
          schema:
            type: string
      responses:
        "200":
          description: Successful response
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/User"
                  pagination:
                    $ref: "#/components/schemas/Pagination"

    post:
      summary: Create user
      operationId: createUser
      tags:
        - Users
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateUserRequest"
      responses:
        "201":
          description: User created
          headers:
            Location:
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/User"
        "422":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"

components:
  schemas:
    User:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string
        email:
          type: string
          format: email
      required:
        - id
        - name
        - email

    CreateUserRequest:
      type: object
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 100
        email:
          type: string
          format: email
      required:
        - name
        - email

13.2 OpenAPI Code Generation

# Auto-generate code from OpenAPI

1. Client SDK generation:
   npx openapi-generator-cli generate \
     -i openapi.yaml \
     -g typescript-fetch \
     -o ./generated-client

2. Server stub generation:
   npx openapi-generator-cli generate \
     -i openapi.yaml \
     -g python-flask \
     -o ./generated-server

3. Mock server:
   npx prism mock openapi.yaml

4. API documentation:
   npx redocly build-docs openapi.yaml

14. API Design Anti-Patterns

14.1 10 Common Mistakes

1. Using verbs in URLs
   Bad:  POST /createUser
   Good: POST /users

2. Inconsistent naming
   Bad:  /users, /getOrders, /product-list
   Good: /users, /orders, /products

3. Singular resource names
   Bad:  /user/123
   Good: /users/123

4. Ignoring HTTP methods
   Bad:  POST /users/123/delete
   Good: DELETE /users/123

5. Always returning 200
   Bad:  200 OK { "error": true, "message": "Not found" }
   Good: 404 Not Found { "title": "Not Found", ... }

6. Excessive nesting
   Bad:  /countries/us/cities/nyc/districts/manhattan/restaurants
   Good: /restaurants?city=nyc&district=manhattan

7. Unversioned API
   Bad:  /users (can change anytime)
   Good: /v1/users

8. No error response standard
   Bad:  { "error": "something went wrong" }
   Good: RFC 7807 Problem Details format

9. No pagination
   Bad:  GET /logs (returns 1 million records)
   Good: GET /logs?limit=50&cursor=abc

10. Missing security headers
    Bad:  Allow HTTP, CORS wide open
    Good: HTTPS required, minimal CORS, proper headers

15. GraphQL vs REST vs gRPC Comparison

+--------------+-------------------+-------------------+-------------------+
|   Feature    |      REST         |     GraphQL       |      gRPC         |
+--------------+-------------------+-------------------+-------------------+
| Protocol     | HTTP/1.1, 2       | HTTP/1.1, 2       | HTTP/2            |
| Data Format  | JSON/XML          | JSON              | Protocol Buffers  |
| Schema       | OpenAPI (optional)| SDL (required)    | .proto (required) |
| Over/Under   | Over-fetching     | Client decides    | Exact schema      |
|   fetching   | possible          |                   |                   |
| Real-time    | WebSocket/SSE     | Subscriptions     | Bidirectional     |
|              |                   |                   | Streaming         |
| Use Cases    | Public APIs,      | Complex data      | Microservices     |
|              | simple CRUD       | relations, mobile | internal comms    |
| Learning     | Low               | Medium            | High              |
| Caching      | HTTP caching easy | Difficult         | None              |
| Error        | HTTP status codes | errors array      | Status codes      |
+--------------+-------------------+-------------------+-------------------+

Selection Guide:

Choose REST:
- Public APIs (for external developers)
- Simple CRUD operations
- HTTP caching needed
- Direct browser calls

Choose GraphQL:
- Multiple clients (web, mobile, IoT)
- Complex data relationships
- Need to solve over/under-fetching
- Rapid frontend development

Choose gRPC:
- Internal microservice communication
- Low latency requirements
- Bidirectional streaming needed
- Strong typed contracts needed

16. Quiz

Q1. REST Principles

Explain why the "Stateless" constraint contributes to scalability.

Answer: The Stateless constraint means the server does not store client state, so each request contains all information needed for processing. This enables:

  1. Simple load balancing: Any server can handle any request (no session stickiness needed)
  2. Easy horizontal scaling: Servers can be added/removed freely
  3. Easy fault recovery: On server failure, another server immediately takes over
  4. Improved caching efficiency: Requests are independent, making cache key generation straightforward

Q2. Pagination

Describe 2 situations where you should choose Cursor pagination over Offset pagination.

Answer:

  1. Large datasets: With millions of records, Offset requires OFFSET 100000 which skips many rows and is very slow. Cursor uses indexes with a WHERE clause to jump directly, maintaining consistent performance regardless of data size.

  2. Real-time data: When data is frequently added or deleted, Offset causes duplicates or misses when re-requesting the same page. Cursor uses the unique identifier of the last seen item as reference point, maintaining consistency.

Q3. Error Handling

Explain the difference between 401 Unauthorized and 403 Forbidden with specific examples.

Answer:

  • 401 Unauthorized (Authentication failure): The client has not proven who they are. For example, calling an API without an Authorization header, or using an expired JWT token. "I don't know who you are. Please log in."

  • 403 Forbidden (Authorization failure): The client is authenticated but lacks permission for the resource. For example, a regular user calling an admin-only API. "I know who you are, but you don't have permission to access this resource."

Q4. Versioning

Compare the pros and cons of URL path versioning (/v1/users) vs Accept header versioning.

Answer: URL Path Versioning (/v1/users):

  • Pros: Intuitive, easy browser testing, easy routing separation, easy caching
  • Cons: Violates REST principles (URL should identify resource location), all URLs change

Accept Header Versioning (Accept: application/vnd.myapi.v2+json):

  • Pros: Clean URLs, follows REST principles, one URL for multiple versions
  • Cons: Testing inconvenient (need curl/Postman), harder debugging, complex caching

In practice, URL path versioning is overwhelmingly more common (GitHub, Stripe, Google, etc.).

Q5. Security

Explain JWT pros/cons and how to respond to token theft.

Answer: Pros: No server session storage needed (Stateless), easy horizontal scaling, easy propagation between microservices, user info in claims

Cons: Hard to revoke tokens (valid until expiration), larger than session IDs, payload not encrypted (only encoded)

Token theft response:

  1. Short Access Token validity (15 minutes)
  2. Refresh Token rotation (issue new token on use, invalidate old)
  3. Token blacklist (store revoked tokens in Redis)
  4. IP/User-Agent binding
  5. Invalidate all Refresh Tokens on suspicious activity detection

References

  1. RFC 7231 - HTTP/1.1 Semantics and Content - https://tools.ietf.org/html/rfc7231
  2. RFC 7807 - Problem Details for HTTP APIs - https://tools.ietf.org/html/rfc7807
  3. OpenAPI Specification 3.1 - https://spec.openapis.org/oas/v3.1.0
  4. JSON:API Specification - https://jsonapi.org/
  5. Google API Design Guide - https://cloud.google.com/apis/design
  6. Microsoft REST API Guidelines - https://github.com/microsoft/api-guidelines
  7. Stripe API Reference - https://stripe.com/docs/api
  8. GitHub REST API - https://docs.github.com/en/rest
  9. Zalando RESTful API Guidelines - https://opensource.zalando.com/restful-api-guidelines/
  10. REST API Design Rulebook - Mark Masse (O'Reilly)
  11. OAuth 2.0 RFC 6749 - https://tools.ietf.org/html/rfc6749
  12. JWT RFC 7519 - https://tools.ietf.org/html/rfc7519
  13. Roy Fielding's Dissertation - https://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm
  14. Richardson Maturity Model - https://martinfowler.com/articles/richardsonMaturityModel.html