FastAPI 시작하기
FastAPI로 빠르고 현대적인 웹 API를 만드는 방법을 배워봅시다.
FastAPI란?
FastAPI는 Python으로 API를 빠르게 개발할 수 있는 현대적인 웹 프레임워크입니다. 타입 힌트를 활용하여 자동으로 데이터 검증과 문서화를 제공합니다.
주요 특징
- 빠른 성능 - Node.js, Go와 비슷한 수준의 속도
- 자동 문서화 - Swagger UI, ReDoc 자동 생성
- 타입 안정성 - Python 타입 힌트 활용
- 비동기 지원 - async/await 완벽 지원
- 쉬운 학습 - 직관적이고 간단한 API
왜 FastAPI인가?
# Flask (전통적)
@app.route('/items/<int:item_id>')
def read_item(item_id):
# 수동으로 검증 필요
if item_id < 0:
return {"error": "Invalid ID"}, 400
return {"item_id": item_id}
# FastAPI (현대적)
@app.get("/items/{item_id}")
def read_item(item_id: int):
# 자동 검증, 자동 문서화
return {"item_id": item_id}
설치하기
# FastAPI 설치
pip install fastapi
# ASGI 서버 (Uvicorn)
pip install "uvicorn[standard]"
# 한 번에 설치
pip install "fastapi[all]"
첫 번째 API
가장 간단한 FastAPI 애플리케이션을 만들어봅시다.
# main.py
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def read_root():
return {"message": "Hello, FastAPI!"}
@app.get("/items/{item_id}")
def read_item(item_id: int):
return {"item_id": item_id}
서버 실행
# 기본 실행
uvicorn main:app --reload
# 포트 지정
uvicorn main:app --reload --port 8080
# 호스트 지정 (외부 접속 허용)
uvicorn main:app --reload --host 0.0.0.0
자동 문서 확인
서버를 실행한 후 다음 URL로 접속하세요:
- Swagger UI: http://localhost:8000/docs
- ReDoc: http://localhost:8000/redoc
- OpenAPI JSON: http://localhost:8000/openapi.json
경로 파라미터
URL 경로에서 값을 추출할 수 있습니다.
from fastapi import FastAPI
app = FastAPI()
# 기본 경로 파라미터
@app.get("/users/{user_id}")
def get_user(user_id: int):
return {"user_id": user_id}
# 여러 개의 경로 파라미터
@app.get("/users/{user_id}/items/{item_id}")
def get_user_item(user_id: int, item_id: str):
return {"user_id": user_id, "item_id": item_id}
# Enum으로 제한
from enum import Enum
class ModelName(str, Enum):
alexnet = "alexnet"
resnet = "resnet"
lenet = "lenet"
@app.get("/models/{model_name}")
def get_model(model_name: ModelName):
if model_name == ModelName.alexnet:
return {"model_name": model_name, "message": "Deep Learning FTW!"}
if model_name.value == "lenet":
return {"model_name": model_name, "message": "LeCNN all the images"}
return {"model_name": model_name, "message": "Have some residuals"}
# 파일 경로 (특수 케이스)
@app.get("/files/{file_path:path}")
def read_file(file_path: str):
return {"file_path": file_path}
경로 파라미터 검증
from fastapi import FastAPI, Path
app = FastAPI()
@app.get("/items/{item_id}")
def read_item(
item_id: int = Path(
..., # 필수 파라미터
title="Item ID",
description="조회할 아이템의 ID",
ge=1, # greater than or equal (>=)
le=1000 # less than or equal (<=)
)
):
return {"item_id": item_id}
쿼리 파라미터
URL의 쿼리 스트링에서 값을 받을 수 있습니다.
from fastapi import FastAPI
from typing import Optional
app = FastAPI()
# 기본 쿼리 파라미터
@app.get("/items/")
def read_items(skip: int = 0, limit: int = 10):
return {"skip": skip, "limit": limit}
# 선택적 파라미터
@app.get("/items/{item_id}")
def read_item(item_id: str, q: Optional[str] = None):
if q:
return {"item_id": item_id, "q": q}
return {"item_id": item_id}
# 여러 값 받기
@app.get("/items/")
def read_items(q: Optional[list[str]] = None):
return {"q": q}
# 요청: /items/?q=foo&q=bar
# 응답: {"q": ["foo", "bar"]}
# 불린 파라미터
@app.get("/items/{item_id}")
def read_item(item_id: str, short: bool = False):
item = {"item_id": item_id}
if not short:
item.update(
{"description": "This is an amazing item"}
)
return item
쿼리 파라미터 검증
from fastapi import FastAPI, Query
app = FastAPI()
@app.get("/items/")
def read_items(
q: Optional[str] = Query(
None,
min_length=3,
max_length=50,
regex="^fixedquery$",
title="Query string",
description="쿼리 파라미터 예시"
),
page: int = Query(1, ge=1),
size: int = Query(10, ge=1, le=100)
):
results = {"page": page, "size": size}
if q:
results.update({"q": q})
return results
Request Body
POST, PUT 요청에서 데이터를 받을 때 사용합니다.
Pydantic 모델
from fastapi import FastAPI
from pydantic import BaseModel, Field
from typing import Optional
app = FastAPI()
# 데이터 모델 정의
class Item(BaseModel):
name: str
description: Optional[str] = None
price: float
tax: Optional[float] = None
@app.post("/items/")
def create_item(item: Item):
item_dict = item.dict()
if item.tax:
price_with_tax = item.price + item.tax
item_dict.update({"price_with_tax": price_with_tax})
return item_dict
# 검증 추가
class ItemWithValidation(BaseModel):
name: str = Field(..., min_length=1, max_length=100)
description: Optional[str] = Field(None, max_length=500)
price: float = Field(..., gt=0, description="가격은 0보다 커야 합니다")
tax: Optional[float] = Field(None, ge=0, le=100)
@app.post("/validated-items/")
def create_validated_item(item: ItemWithValidation):
return item
중첩된 모델
from pydantic import BaseModel, HttpUrl
from typing import Optional, List, Set
class Image(BaseModel):
url: HttpUrl
name: str
class Item(BaseModel):
name: str
description: Optional[str] = None
price: float
tax: Optional[float] = None
tags: Set[str] = set()
images: Optional[List[Image]] = None
@app.post("/items/")
def create_item(item: Item):
return item
# 요청 예시:
# {
# "name": "Foo",
# "price": 45.2,
# "tags": ["electronics", "computers"],
# "images": [
# {"url": "http://example.com/img1.jpg", "name": "Image 1"},
# {"url": "http://example.com/img2.jpg", "name": "Image 2"}
# ]
# }
Body + Path + Query 파라미터
@app.put("/items/{item_id}")
def update_item(
item_id: int, # Path 파라미터
item: Item, # Body
q: Optional[str] = None # Query 파라미터
):
result = {"item_id": item_id, **item.dict()}
if q:
result.update({"q": q})
return result
응답 모델
응답의 형식을 정의하고 불필요한 데이터를 숨길 수 있습니다.
from pydantic import BaseModel, EmailStr
from typing import Optional
class UserIn(BaseModel):
username: str
password: str
email: EmailStr
full_name: Optional[str] = None
class UserOut(BaseModel):
username: str
email: EmailStr
full_name: Optional[str] = None
@app.post("/users/", response_model=UserOut)
def create_user(user: UserIn):
# 비밀번호는 응답에서 제외됨
return user
# 응답 상태 코드 지정
from fastapi import status
@app.post("/users/", response_model=UserOut, status_code=status.HTTP_201_CREATED)
def create_user(user: UserIn):
return user
# 리스트 응답
@app.get("/users/", response_model=List[UserOut])
def read_users():
return [
{"username": "john", "email": "john@example.com"},
{"username": "jane", "email": "jane@example.com"}
]
에러 처리
적절한 에러 응답을 반환할 수 있습니다.
from fastapi import FastAPI, HTTPException, status
app = FastAPI()
items = {"foo": "The Foo Wrestlers"}
@app.get("/items/{item_id}")
def read_item(item_id: str):
if item_id not in items:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Item not found",
headers={"X-Error": "There goes my error"}
)
return {"item": items[item_id]}
# 커스텀 예외
class ItemNotFoundException(Exception):
def __init__(self, item_id: str):
self.item_id = item_id
@app.exception_handler(ItemNotFoundException)
def item_not_found_handler(request, exc: ItemNotFoundException):
return JSONResponse(
status_code=404,
content={"message": f"Item {exc.item_id} not found"}
)
@app.get("/items/{item_id}")
def read_item(item_id: str):
if item_id not in items:
raise ItemNotFoundException(item_id=item_id)
return {"item": items[item_id]}
의존성 주입
코드를 재사용하고 관심사를 분리할 수 있습니다.
from fastapi import Depends, FastAPI, HTTPException
from typing import Optional
app = FastAPI()
# 간단한 의존성
def common_parameters(q: Optional[str] = None, skip: int = 0, limit: int = 100):
return {"q": q, "skip": skip, "limit": limit}
@app.get("/items/")
def read_items(commons: dict = Depends(common_parameters)):
return commons
@app.get("/users/")
def read_users(commons: dict = Depends(common_parameters)):
return commons
# 클래스 기반 의존성
class CommonQueryParams:
def __init__(self, q: Optional[str] = None, skip: int = 0, limit: int = 100):
self.q = q
self.skip = skip
self.limit = limit
@app.get("/items/")
def read_items(commons: CommonQueryParams = Depends()):
return commons
인증 의존성
from fastapi import Depends, HTTPException, Header
from typing import Optional
def verify_token(x_token: str = Header(...)):
if x_token != "fake-super-secret-token":
raise HTTPException(status_code=400, detail="X-Token header invalid")
return x_token
def verify_key(x_key: str = Header(...)):
if x_key != "fake-super-secret-key":
raise HTTPException(status_code=400, detail="X-Key header invalid")
return x_key
@app.get("/items/", dependencies=[Depends(verify_token), Depends(verify_key)])
def read_items():
return [{"item": "Foo"}, {"item": "Bar"}]