Files
todos2/.claude/skills/api-design-standards.md
jungwoo choi b54811ad8d Initial commit: 프로젝트 초기 구성
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 06:53:16 +09:00

6.7 KiB

RESTful API 설계 규칙 (API Design Standards)

이 프로젝트의 RESTful API 설계 패턴입니다.

FastAPI 기본 구조

앱 초기화

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import Optional

app = FastAPI(
    title="Biocode API",
    description="바이오코드 성격 분석 및 유명인 등록 API",
    version="1.0.0"
)

Pydantic 모델 정의

class AddFamousPersonRequest(BaseModel):
    year: int
    month: int
    day: int
    name: str
    description: Optional[str] = ""
    entity_type: Optional[str] = "person"  # "person" or "organization"

class BiocodeResponse(BaseModel):
    biocode: str
    g_code: str
    s_code: str
    personality: dict

엔드포인트 패턴

Health Check

@app.get("/health")
async def health_check():
    """헬스체크 엔드포인트"""
    return {"status": "healthy", "timestamp": datetime.now().isoformat()}

GET - 조회

@app.get("/biocode/{year}/{month}/{day}")
async def get_biocode(year: int, month: int, day: int):
    """생년월일로 바이오코드 조회"""
    try:
        biocode = calculate_biocode(year, month, day)
        g_code = f"g{biocode[:2]}"
        s_code = biocode[2:]

        if g_code not in biocode_data:
            raise HTTPException(status_code=404, detail=f"G code {g_code} not found")

        personality = biocode_data[g_code].get("codes", {}).get(biocode, {})

        return {
            "biocode": biocode,
            "g_code": g_code,
            "s_code": s_code,
            "personality": personality
        }
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

POST - 생성

@app.post("/add_famous_person")
async def add_famous_person(request: AddFamousPersonRequest):
    """유명인/조직 등록"""
    try:
        biocode = calculate_biocode(request.year, request.month, request.day)
        g_code = f"g{biocode[:2]}"

        if g_code not in biocode_data:
            raise HTTPException(status_code=404, detail=f"G code {g_code} not found")

        # 엔티티 타입에 따라 저장 위치 결정
        is_organization = request.entity_type == "organization"
        target_field = "famousOrganizations" if is_organization else "famousPeople"

        # 데이터 추가
        new_entry = {
            "name": request.name,
            "code": biocode,
            "description": request.description or ""
        }

        if target_field not in biocode_data[g_code]["codes"][biocode]:
            biocode_data[g_code]["codes"][biocode][target_field] = []

        biocode_data[g_code]["codes"][biocode][target_field].append(new_entry)

        # JSON 파일 저장
        save_biocode_data(g_code)

        return {
            "success": True,
            "biocode": biocode,
            "entity_type": request.entity_type,
            "name": request.name
        }
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

라우터 분리 패턴

메인 앱 (main.py)

from fastapi import FastAPI
from app.routers import users, articles

app = FastAPI(title="News Engine API")

app.include_router(users.router, prefix="/api/users", tags=["users"])
app.include_router(articles.router, prefix="/api/articles", tags=["articles"])

@app.get("/health")
async def health():
    return {"status": "healthy"}

라우터 (routers/users.py)

from fastapi import APIRouter, HTTPException, Depends
from app.models.user import UserCreate, UserResponse
from app.database import get_database

router = APIRouter()

@router.post("/", response_model=UserResponse)
async def create_user(user: UserCreate, db = Depends(get_database)):
    """새 사용자 생성"""
    existing = await db.users.find_one({"email": user.email})
    if existing:
        raise HTTPException(status_code=400, detail="Email already registered")

    result = await db.users.insert_one(user.dict())
    return UserResponse(id=str(result.inserted_id), **user.dict())

@router.get("/{user_id}", response_model=UserResponse)
async def get_user(user_id: str, db = Depends(get_database)):
    """사용자 조회"""
    user = await db.users.find_one({"_id": ObjectId(user_id)})
    if not user:
        raise HTTPException(status_code=404, detail="User not found")
    return UserResponse(**user)

에러 처리

HTTPException 사용

from fastapi import HTTPException

# 404 Not Found
raise HTTPException(status_code=404, detail="Resource not found")

# 400 Bad Request
raise HTTPException(status_code=400, detail="Invalid input data")

# 500 Internal Server Error
raise HTTPException(status_code=500, detail=str(e))

전역 예외 처리

from fastapi import Request
from fastapi.responses import JSONResponse

@app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception):
    return JSONResponse(
        status_code=500,
        content={"detail": "Internal server error", "error": str(exc)}
    )

응답 형식

성공 응답

{
    "success": true,
    "data": { ... },
    "message": "Operation completed successfully"
}

에러 응답

{
    "detail": "Error message here"
}

목록 응답

{
    "items": [...],
    "total": 100,
    "page": 1,
    "page_size": 20
}

CORS 설정

from fastapi.middleware.cors import CORSMiddleware

app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:3000", "https://yourdomain.com"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

API 버저닝

# URL 기반 버저닝
app.include_router(v1_router, prefix="/api/v1")
app.include_router(v2_router, prefix="/api/v2")

# 또는 헤더 기반
@app.get("/api/resource")
async def get_resource(api_version: str = Header(default="v1")):
    if api_version == "v2":
        return v2_response()
    return v1_response()

인증

JWT 토큰 검증

from fastapi import Depends, HTTPException
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
import jwt

security = HTTPBearer()

async def verify_token(credentials: HTTPAuthorizationCredentials = Depends(security)):
    try:
        payload = jwt.decode(
            credentials.credentials,
            os.getenv("JWT_SECRET_KEY"),
            algorithms=["HS256"]
        )
        return payload
    except jwt.ExpiredSignatureError:
        raise HTTPException(status_code=401, detail="Token expired")
    except jwt.InvalidTokenError:
        raise HTTPException(status_code=401, detail="Invalid token")

@router.get("/protected")
async def protected_route(user = Depends(verify_token)):
    return {"user": user}