# RESTful API 설계 규칙 (API Design Standards) 이 프로젝트의 RESTful API 설계 패턴입니다. ## FastAPI 기본 구조 ### 앱 초기화 ```python 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 모델 정의 ```python 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 ```python @app.get("/health") async def health_check(): """헬스체크 엔드포인트""" return {"status": "healthy", "timestamp": datetime.now().isoformat()} ``` ### GET - 조회 ```python @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 - 생성 ```python @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) ```python 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) ```python 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 사용 ```python 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)) ``` ### 전역 예외 처리 ```python 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)} ) ``` ## 응답 형식 ### 성공 응답 ```json { "success": true, "data": { ... }, "message": "Operation completed successfully" } ``` ### 에러 응답 ```json { "detail": "Error message here" } ``` ### 목록 응답 ```json { "items": [...], "total": 100, "page": 1, "page_size": 20 } ``` ## CORS 설정 ```python 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 버저닝 ```python # 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 토큰 검증 ```python 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} ```