어떤 프로그램이 다른 프로그램의 기능을 사용할 수 있게 해주는 약속된 방법
FastAPI는 파이썬 언어를 위해 설계된 웹 프레임워크로, 특히 API 개발에 최적화 되어 있다.
현대적이고, 빠르며(고성능), 파이썬 표준 타입 힌트에 기초한 Python의 API를 빌드하기 위한 웹 프레임워크
uvicorn main:app ,uvicorn main:app --reloadmain:app : main.py 안에 있는 app = FastAPI() 객체를 실행한다는 뜻--reload : 코드 수정하면 서버 자동으로 다시 시작 (개발용 필수)"경로"는 첫 번째 / 부터 시작하는 URL의 뒷부분을 의미(ex: https://example.com/~)
HTTP 프로토콜에서는 아래와 같은 "메소드"를 하나(또는 이상) 사용하여 각 경로와 통신할 수 있다.
POST : 데이터를 생성하기 위해.GET : 데이터를 읽기 위해.PUT : 데이터를 수정하기 위해.DELETE : 데이터를 삭제하기 위해.
@app.get("/"): FastAPI에게 바로 아래에 있는 함수가 다음으로 이동하는 요청을 처리한다는 것을 알림@something : 데코레이터라 부르고 함수의 맨위에 놓는다.@app.get("/")는 FastAPI에게 아래 함수가 경로 /의 get 작동에 해당한다고 알려줌@app.post() / @app.put() / @app.delete() 다 가능/get@app.get("/") 아래)from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def root():
return {"message": "Hello World"}
/ 에 대한 GET 작동 을 사용하는 요청을 받을 때마다 FastAPI 에 의해 호출

from fastapi import FastAPI
app = FastAPI()
@app.get("/users/me")
async def read_user_me():
return {"user_id": "the current user"}
@app.get("/users/{user_id}")
async def read_user(user_id: str):
return {"user_id": user_id}
/users/{user_id} 이전에 /users/me 를 먼저 선언해야 함/users/me 요청 또한 매개변수 user_id 의 값이 me 인 것으로 여기게 됨
/files/{file_path}file_path 자체가 home/johndoe/myfile.txt 와 같은 경로를 포함해야 함/files/home/johndoe/myfile.txt/files/{file_path:path} 이러한 URL을 사용함으로써 path를 포함하는 경로 매개변수를 선언할 수 있다file_path, :path는 매개변수가 경로와 일치해야 함을 명시 from fastapi import FastAPI
app = FastAPI()
@app.get("/files/{file_path:path}")
async def read_file(file_path: str):
return {"file_path": file_path}
/files/ 뒤에 나오는 모든 경로(슬래시 포함)를 file_path 변수 로 받아라.:path 변환기(converter) @app.get("/users/{user_id}")
async def get_user(user_id: str):
/users/123 → user_id="123" 가능/users/123/profile 같은 경로는 불가능 http://127.0.0.1:8000/items/?skip=0&limit=10
http://127.0.0.1:8000/items/ = http://127.0.0.1:8000/items/?skip=0&limit=10http://127.0.0.1:8000/items/?skip=20from typing import Union
from fastapi import FastAPI
app = FastAPI()
@app.get("/items/{item_id}")
async def read_item(item_id: str, q: Union[str, None] = None):
if q:
return {"item_id": item_id, "q": q}
return {"item_id": item_id}
q: str | None = None (신버전) , q: Union[str, None] = None (구버전)Union[str, None] → q는 문자열(str)이거나 None일 수 있다= None → 기본값은 None (즉, 안 보내면 None으로 자동 처리)| 요청 URL | item_id 값 | q 값 | 반환 결과 |
|---|---|---|---|
/items/foo | "foo" | None | {"item_id": "foo"} |
/items/foo?q=bar | "foo" | "bar" | {"item_id": "foo", "q": "bar"} |
/items/123?q=test | "123" | "test" | {"item_id": "123", "q": "test"} |
| 이름 | 위치 | 타입 | 필수 | 설명 |
|---|---|---|---|---|
item_id | path | string | ✅ | 아이템의 고유 ID |
q | query | string | ❌ | 검색어 / 추가 필터 |

| 매개변수 | 타입 | 역할 | 값이 들어오는 위치 | 기본값 | 설명 |
|---|---|---|---|---|---|
item_id | str | 경로 매개변수 | /items/{item_id} | ❌ (필수) | 어떤 아이템인지 구분하는 고유값 |
q | Union[str, None] | 쿼리 매개변수 | ?q=... | None | 선택적인 검색어나 추가 필터 |
short | bool | 쿼리 매개변수 | ?short=true | False | 설명을 짧게 할지 여부 (True면 짧게) |
| 요청 URL | item_id | q | short | 반환 결과 |
|---|---|---|---|---|
/items/apple | "apple" | None | False | {"item_id": "apple", "description": "...long description"} |
/items/apple?q=red | "apple" | "red" | False | {"item_id": "apple", "q": "red", "description": "...long description"} |
/items/apple?q=red&short=true | "apple" | "red" | True | {"item_id": "apple", "q": "red"} |
from typing import Union
from fastapi import FastAPI
app = FastAPI()
@app.get("/users/{user_id}/items/{item_id}")
async def read_user_item(
user_id: int, item_id: str, q: Union[str, None] = None, short: bool = False
):
item = {"item_id": item_id, "owner_id": user_id}
if q:
item.update({"q": q})
if not short:
item.update(
{"description": "This is an amazing item that has a long description"}
)
return item
여러 경로 매개변수와 쿼리 매개변수를 동시에 선언할 수 있으며 FastAPI는 어느 것이 무엇인지 알고 있음
user_id: int, item_id: str, q: Union[str, None] = None, short: bool = False| 구분 | 예시 | 순서가 중요한가? | 설명 |
|---|---|---|---|
| ① 경로 작성 순서 (URL Path Pattern) | /users/{user_id}/items/{item_id} | 중요 ✅ | FastAPI가 어떤 라우트로 요청을 연결할지 결정함 |
| ② 함수 매개변수 순서 (Parameter order) | (user_id: int, item_id: str, q: str None = None, short: bool = False) | 중요 ❌ | FastAPI가 “이름” 기준으로 자동 매핑함 |

| None = None 은 “이 값이 없어도 된다”는 뜻 : 이 필드는 선택(optional)
{
"name": "Apple",
"description": "Fresh red apple",
"price": 3.5,
"tax": 0.1
}
item = Item(name="Apple", description="Fresh red apple", price=3.5, tax=0.1)


PUT /items/5 : item_id = 5return {"item_id": item_id, **item.dict()}**item.dict() : Item 모델을 딕셔너리로 변환해서 그 내용을 펼쳐서(**) 병합하는 코드
# js
{
"name": "MacBook Pro",
"description": "Apple laptop",
"price": 2500.0,
"tax": 250.0
}
# python
item = Item(name="MacBook Pro", description="Apple laptop", price=2500.0, tax=250.0)
# **item.dict() 응답
{
"item_id": 5,
"name": "MacBook Pro",
"description": "Apple laptop",
"price": 2500.0,
"tax": 250.0
}
from fastapi import FastAPI
from pydantic import BaseModel
class Item(BaseModel):
name: str
description: str | None = None
price: float
tax: float | None = None
app = FastAPI()
@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item, q: str | None = None):
result = {"item_id": item_id, **item.dict()}
if q:
result.update({"q": q})
return result
| 옵션 | 역할 / 의미 | Swagger 문서에서의 표시 | 실제 동작 / 검증 규칙 | 요청 예시 | 결과 / 비고 |
|---|---|---|---|---|---|
alias | 내부 변수명(q)과는 다른 이름으로 쿼리 파라미터를 받을 수 있게 함 | Parameter 이름이 alias 값으로 표시됨 | URL 쿼리 키 이름이 alias로 지정되어야 함 | /items/?item-query=fixedquery | 내부 코드에서는 q로 사용되지만, 외부 요청에서는 반드시 item-query로 전달해야 함 |
title | Swagger 문서에서 해당 파라미터의 제목(Title) 을 표시 | Parameters 섹션의 title 항목으로 노출 | 코드 실행에는 영향 없음 (문서용) | - | 문서 가독성을 위해 제목만 지정할 때 사용 |
description | Swagger 문서에서 해당 파라미터의 상세 설명 추가 | Parameters → Description 부분에 표시됨 | 코드 실행에는 영향 없음 (문서용) | - | “이 파라미터가 무슨 역할인지” 안내용 텍스트 |
min_length, max_length | 문자열의 최소·최대 길이를 지정 | Schema 영역에 (minLength=3, maxLength=50) 로 표시 | 입력값이 범위를 벗어나면 422 에러 반환 | /items/?item-query=hi → ❌ | "msg": "String should have at least 3 characters" |
pattern | 정규식(Regular Expression)으로 입력값 검증 | Schema 영역에 pattern="^fixedquery$" 표시 | 해당 패턴과 완전히 일치해야 통과 | /items/?item-query=fixedquery ✅/items/?item-query=fixedquery123 ❌ | 유효하지 않으면 "String should match pattern" 에러 |
deprecated | Swagger 문서에 “⚠️ Deprecated” 표시 | Parameter 옆에 (deprecated) 표기 및 노란색 경고 아이콘 표시 | 기능은 그대로 작동함 (제한 없음) | /items/?item-query=fixedquery ✅ | 사용은 가능하지만, 문서상 “더 이상 권장되지 않음” 표시 |
default=None | 기본값을 None으로 설정 (즉, 선택적 파라미터) | Schema에 "nullable": true, "default": null 로 표시 | 값이 없어도 요청 가능 (/items/ OK) | /items/ → ✅ | 필수가 아니며, 전달되지 않으면 q=None |
from typing import Union
from fastapi import FastAPI
app = FastAPI()
@app.get("/items/")
async def read_items(q: Union[str, None] = None):
results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
if q:
results.update({"q": q})
return results
Optional[str] 자료형 = str | None 과 동일한 표현# 기본 파이썬 방식(단순 변수만 정의)
@app.get("/items/")
async def read_items(q: str | None = None):
return {"q": q}
# FastAPI의 Query() 방식 (고급 제어 기능 추가)
from fastapi import Query
@app.get("/items/")
async def read_items(q: str | None = Query(default=None, max_length=50, min_length=2, description="검색어")):
return {"q": q}


q: Union[str, None] = Query(
default=None, min_length=3, max_length=50, pattern="^fixedquery$"
),
(q: str = Query(default="fixedquery", min_length=3))




| 구분 | Path | Query |
|---|---|---|
| 전달 방식 | URL 경로 일부 (/items/{item_id}) | URL 뒤 파라미터 (?q=search) |
| 필수 여부 | 항상 필수 | 기본값 없으면 필수, default=None이면 선택 |
| 사용 목적 | 리소스 식별 (ex. /users/5) | 검색, 필터링, 옵션 (ex. ?sort=desc) |
| FastAPI 함수 | Path() | Query() |

from fastapi import FastAPI, Path
app = FastAPI()
@app.get("/items/{item_id}")
# 순서변경
async def read_items(q: str, item_id: int = Path(title="The ID of the item to get")):
results = {"item_id": item_id}
if q:
results.update({"q": q})
return results
* 를 함수의 첫 번째 매개변수로 전달하면, * 를 넣으면 “이 뒤의 매개변수들은 위치로 받지 말고 이름으로만(keyword) 받아라.” 라고 알려줌
async def read_items(item_id: int = Path(title="The ID"), q: str):item_id: int = Path(...) → 기본값이 있다고 간주q: str → 기본값이 없음 (즉, 필수 인자) = 필수인자가 앞async def read_items(*, item_id: int = Path(title="The ID of the item to get"), q: str):gt : 크거나(greater than) / ge : 크거나 같은(greater equal)lt : 작거나(less than) / le : 작거나 같은(less than or equal)

order_by: Literal["created_at", "updated_at"]limit: int = Field(100, gt=0, le=100)filter_query: Annotated[FilterParams, Query()]Annotated[FilterParams, Query()]
extra 필드를 forbid 로 설정할 수 있음
model_config = {"extra": "forbid"}https://example.com/items/?limit=10&tool=plumbus
item: Union[Item, None] = None
importance: int = Body()importance: int = Body()

| 구분 | 사용 위치 | 역할 | 예시 |
|---|---|---|---|
🧩 Query() | 함수의 쿼리 매개변수 | /items/?q=search 처럼 URL 쿼리로 전달 | q: str = Query(default="hello") |
🧩 Path() | 함수의 경로 매개변수 | /items/{item_id}처럼 URL 일부로 전달 | item_id: int = Path(gt=0) |
🧩 Body() | 함수의 요청 본문(body) | JSON body에서 값 받기 | data: dict = Body() |
🧱 Field() | 클래스 내부(BaseModel) | JSON body에서 받을 값의 제약조건, 설명, 기본값 | price: float = Field(gt=0, description="가격") |
Query, Path와 Body를 사용해 경로 작동 함수 매개변수 내에서 추가적인 검증이나 메타데이터를 선언한 것처럼 Pydantic의 Field를 사용하여 모델 내에서 검증과 메타데이터를 선언할 수 있음

class Item(BaseModel):
name: str
description: Union[str, None] = None
price: float
tax: Union[float, None] = None
tags: list = []
tags: list = []= [] : 기본값이 빈 리스트| 표현 | 사용 가능 여부 | 설명 |
|---|---|---|
list | ✅ 가능 (Python 기본) | 리스트임을 의미하지만, 내부 요소의 타입을 지정하지 않음 |
list[str] | ✅ 권장 (Python 3.9+) | 리스트의 요소 타입(str) 을 명확히 지정 |
List[str] | ✅ 가능 (Python 3.8 이하, 또는 구버전 호환) | typing 모듈에서 가져온 제네릭 리스트 (from typing import List) |

from typing import Set 필요tags: Set[str] = set()Set[str] : 요소가 문자열(str) 인 집합(set) 타입set() : 기본값은 빈 집합 {}
class Image(BaseModel):
url: str
name: str
class Item(BaseModel):
name: str
description: Union[str, None] = None
price: float
tax: Union[float, None] = None
tags: Set[str] = set()
image: Union[Image, None] = None

타입 공식문서: https://docs.pydantic.dev/latest/concepts/types/
url: HttpUrl 은 단순 문자열이 아니라 URL 형식이 올바른지 자동 검증해주는 타입| 타입 | 의미 | 예시 |
|---|---|---|
Image | 하나의 이미지 객체 | "image": {"url": "...", "name": "..."} |
List[Image] | 여러 이미지 객체 | "images": [{"url": "...", "name": "..."}, {...}] |



| 장점 | 설명 |
|---|---|
| 데이터 구조 명확 | 어떤 형태로 데이터를 보내야 하는지 한눈에 알 수 있음 |
| 자동 검증 | 타입이 틀리면 FastAPI가 알아서 422 에러 반환 |
| 자동 변환 | "1": "0.5" → {1: 0.5} 로 자동 캐스팅 |
| 문서 자동화 | Swagger에 {int: float} 구조 자동 표시 |
| 유지보수 용이 | 코드만 봐도 데이터 의도 명확 (“id별 가중치구나”) |



Path() / Query() / Header() / Cookie() / Body() / Form() / File() 중 사용

일반적인 Sweager에서는 다중 예제를 지원하지 않음

start_datetime: Annotated[datetime, Body()]start_process = start_datetime + process_afterduration = end_datetime - start_process| 타입 | 역할 | 예시 값 | 자동 변환 |
|---|---|---|---|
UUID | 고유 식별자 | "550e8400-e29b-41d4-a716-446655440000" | 문자열 → UUID 객체 |
datetime | 날짜 + 시간 | "2025-10-11T09:00:00" | 문자열 → datetime |
time | 시각만 | "10:30:00" | 문자열 → time |
timedelta | 시간 차이 | 3600.0 | 숫자(초) → timedelta(hours=1) |
| 매개변수 | 타입 | 데이터 출처 | 설명 |
|---|---|---|---|
item_id | UUID | 경로(/items/{item_id}) | 아이템의 고유 식별자 (UUID 형식) |
start_datetime | datetime | Body | 작업 시작 시간 |
end_datetime | datetime | Body | 작업 종료 시간 |
process_after | timedelta | Body | 시작 시간 이후 얼마 뒤에 처리 시작할지 (시간 차) |
repeat_at | time \| None | Body (선택) | 반복 실행 시간이 있을 경우 특정 시각 지정 |
UUIDdatetime.datetimedatetime.datedatetime.timedatetime.timedeltafrozensetbytesDecimal
ads_id: Annotated[str | None, Cookie()] = None
async def read_items(headers: Annotated[CommonHeaders, Header()]):headers: Annotated[CommonHeaders, Header()]| 구분 | 가져오는 위치 | 예시 URL / 요청 | 사용 예시 (FastAPI 코드) | 설명 | 기본 특징 | |
|---|---|---|---|---|---|---|
| 🟩 Query | ? 뒤의 쿼리스트링 | /items/?q=apple&limit=5 | `q: Annotated[str | None, Query()]` | URL에 붙는 데이터 (검색, 필터용) | 선택적(기본값 설정 가능) |
| 🟦 Path | 경로 일부 | /users/123 | user_id: Annotated[int, Path()] | 리소스 식별용 (URL 안의 변수) | 필수 (URL 구조와 일치해야 함) | |
| 🟨 Header | HTTP 요청 헤더 | User-Agent: ChromeAccept: application/json | user_agent: Annotated[str, Header()] | 브라우저나 API 클라이언트가 보낸 부가 정보 | 자동으로 대소문자 무시 | |
| 🟧 Cookie | 브라우저 쿠키 | Cookie: session_id=abc123 | `session_id: Annotated[str | None, Cookie()]` | 브라우저가 저장하고 자동으로 보내는 값 | 서버가 Set-Cookie로 생성 가능 |
| 🟥 Body | 요청 본문 (JSON/Form) | {"username": "kihoon", "age": 25} | data: Annotated[UserCreate, Body()] | API에서 가장 자주 쓰는 실제 데이터 | POST/PUT/PATCH 등에서 사용 |
| 종류 | 요청 예시 | 서버 코드 | 서버가 받는 값 |
|---|---|---|---|
| Query | /items/?q=apple | q: Annotated[str, Query()] | "apple" |
| Path | /users/42 | user_id: Annotated[int, Path()] | 42 |
| Header | User-Agent: Chrome | ua: Annotated[str, Header(alias="User-Agent")] | "Chrome" |
| Cookie | Cookie: ads_id=abc123 | ads_id: Annotated[str, Cookie()] | "abc123" |
| Body | { "username": "kim" } | data: Annotated[User, Body()] | User(username="kim") |

| 구분 | 역할 | 데이터 방향 | 시점 | 목적 |
|---|---|---|---|---|
item: Item | 요청 본문(JSON)을 검증 | 클라이언트 → 서버 | 요청 시 | 클라이언트가 보낸 데이터를 올바른 구조로 받기 |
response_model=Item | 응답 데이터를 검증·필터링 | 서버 → 클라이언트 | 응답 시 | 서버가 돌려줄 데이터를 정해진 형태로 보장하기 |


pip install pydantic[email] or poetry add "pydantic[email]"

[], False 같은 기본값(비어 있는 값)은 “None이 아니기 때문에” 그대로 응답에 포함

user_in.dict()
**user_in.dict()

UserInDB(**user_in.dict(), hashed_password=hashed_password)





@app.post("/users/", status_code=status.HTTP_201_CREATED)
@app.delete("/users/{id}", status_code=status.HTTP_204_NO_CONTENT)
@app.get("/users/{id}", status_code=status.HTTP_200_OK)
HTTP_200_OK
HTTP_201_CREATED
HTTP_400_BAD_REQUEST
HTTP_404_NOT_FOUND
HTTP_500_INTERNAL_SERVER_ERROR


Annotated[str, Form()]<form></form>)이 데이터를 서버로 보내는 방식은 일반적으로 해당 데이터에 대해 "특수" 인코딩을 사용 (JSON과 다름)<form> 태그는 JSON을 전송하지 않음
<form>으로 보내는 요청pip install python-multipart or poetry add python-multipartfrom fastapi import FastAPI, FormHTML <form>에서 전송되는 데이터를 처리하기 위한 타입
| 항목 | Body() 사용 시 | Form() 사용 시 |
|---|---|---|
| Swagger 입력창 | JSON 코드 영역 | 폼 입력 필드 여러 개 |
| Content-Type | application/json | application/x-www-form-urlencoded 또는 multipart/form-data |
| 용도 | API 호출 시 JSON 데이터 전송 | 웹 폼, 로그인 등 form 전송 처리용 |
| 눈에 띄는 변화 | JSON 입력창 → 필드 입력 UI로 변경 | ✅ 있음 (입력 방식 변화) |
from typing import Annotated
from fastapi import FastAPI, Form
from pydantic import BaseModel
app = FastAPI()
class FormData(BaseModel):
username: str
password: str
model_config = {"extra": "forbid"} # ✅
@app.post("/login/")
async def login(data: Annotated[FormData, Form()]):
return data
# 결과
username: Rick
password: Portal Gun
extra: Mr. Poopybutthole
# 이렇게 추가 하려고 하면 extra 필드가 허용되지 않는다는 오류 응답을 받게 됨
File을 사용하여 클라이언트가 업로드할 파일들을 정의 가능python-multipart 설치 필요from fastapi import FastAPI, File, UploadFileBody 및 Form 과 동일한 방식으로 파일의 매개변수를 생성async def create_file(file: bytes = File()):File 은 Form 으로부터 직접 상속된 클래스File의 본문을 선언할 때, 매개변수가 쿼리 매개변수 또는 본문(JSON) 매개변수로 해석되는 것을 방지하기 위해 File 을 사용해야 한다.
async def create_upload_file(file: UploadFile):"스풀 파일"을 사용
업로드 된 파일의 메타데이터를 얻을 수 있다.
file-like async 인터페이스를 가지고 있음
read(), write(), seek(), close() 같은 메서드를 갖고 있어서 UploadFile 내부는 비동기 파일 핸들러(SpooledTemporaryFile)를 감싸고 있고,
read, write, seek, close가 모두 비동기(async def) 버전으로 정의
contents = await myfile.read()contents = myfile.file.read()<form></form>)이 서버에 데이터를 전송하는 방식은 대개 데이터에 JSON과는 다른 "특별한" 인코딩을 사용application/x-www-form-urlencoded 을 사용해 인코딩 multipart/form-data 로 인코딩application/json 가 아닌 multipart/form-data 로 인코딩 됨Body 필드 를 함께 선언할 수는 없다.
async def create_files(files: List[bytes] = File()):async def create_upload_files(files: List[UploadFile]):File 과 Form 을 사용하여 파일과 폼을 함께 정의할 수 있다.poetry add python-multipart 설치 필요

from fastapi import FastAPI, HTTPException 필요

@app.exception_handler()from fastapi.responses import JSONResponse

@app.exception_handler(RequestValidationError)

from fastapi.exception_handlers import (
http_exception_handler,
request_validation_exception_handler,
)
| 표현 | 실제 호출되는 함수 | 예시 출력 | 용도 |
|---|---|---|---|
{exc} | str(exc) | "Nope! I don't like 3." | 사용자가 보기 좋음 |
{repr(exc)} | repr(exc) | "HTTPException(status_code=418, detail='Nope! I don't like 3.')" | 디버깅용 (어떤 예외 객체인지 한눈에 확인 가능) |





deprecated 매개변수를 전달하면 됨
from fastapi.encoders import jsonable_encoder 필요jsonable_encoder()jsonable_encoder가 Pydantic 모델과 같은 객체를 받고 JSON 호환 가능한 버전으로 반환

| 구분 | original | encoded |
|---|---|---|
| 타입 | Item (Pydantic 모델) | dict (JSON 호환 데이터) |
| 직렬화 여부 | ❌ Python 객체 그대로 | ✅ JSON으로 변환 가능한 형태 |
| Swagger에서 보이는 형태 | 같아 보임 | 같아 보임 |
| 실제 내부 데이터 구조 | datetime, set, UUID 등 Python 타입 그대로 | "2025-10-16T21:20:00" 같은 문자열 형태 |





item.model_dump(exclude_unset=True)stored_item_model.model_copy(update=update_data):

| 구분 | 클래스(직접 호출) | Depends(의존성 주입) |
|---|---|---|
| 호출 방식 | 직접 Common().do_something() 호출 | FastAPI가 알아서 호출 |
| 실행 시점 | 코드에서 명시적으로 호출 | 요청(request) 들어올 때마다 자동 |
| 주입 구조 | 개발자가 직접 인스턴스 생성 | FastAPI가 알아서 생성하고 파라미터 주입 |
| 스코프 관리 | 수동으로 제어해야 함 (전역, 지역 등) | 요청 단위(request-scope), 앱 단위(singleton) 자동 관리 가능 |
| 테스트 | mock 만들기 귀찮음 | 의존성 오버라이드로 손쉽게 대체 가능 |
| 유지보수 | 코드 간 결합도 높음 | 결합도 낮음 (loosely coupled) |
async def common_parameters(q: str | None = None, skip: int = 0, limit: int = 100):
return {"q": q, "skip": skip, "limit": limit}
from fastapi import Depends, FastAPIDepends()는 “이 매개변수는 이 함수를 먼저 실행한 결과를 넣어줘” 라는 명령
commons: Annotated[dict, Depends(common_parameters)]
commons: Annotated[dict, Depends(common_parameters)]from typing import Annotated
from fastapi import FastAPI, Depends
app = FastAPI()
async def common_parameters(q: str | None = None, skip: int = 0, limit: int = 100):
return {"q": q, "skip": skip, "limit": limit}
@app.get("/items/")
async def read_items(commons: Annotated[dict, Depends(common_parameters)]): ⭐️
return commons
# 결과
{
"q": "apple",
"skip": 10,
"limit": 5
}
CommonsDep = Annotated[dict, Depends(common_parameters)] ⭐️
@app.get("/users/")
async def read_users(commons: CommonsDep):
return commons
class Cat:
def __init__(self, name: str):
self.name = name
fluffy = Cat(name="Mr Fluffy")

commons: CommonQueryParams = Depends(CommonQueryParams)commons = Depends(CommonQueryParams) 이렇게 작성해도 무방함commons: CommonQueryParams = Depends(CommonQueryParams)commons: CommonQueryParams = Depends() 이렇게 단축하는것도 가능@app.get("/items/")
async def read_items(commons: CommonQueryParams = Depends(CommonQueryParams)):
response = {}
if commons.q:
response.update({"q": commons.q})
items = fake_items_db[commons.skip : commons.skip + commons.limit]
response.update({"items": items})
return response
-------------------------------------------------------------------------------
@app.get("/items/")
async def read_items(commons: CommonQueryParams = Depends()):
response = {}
if commons.q:
response.update({"q": commons.q})
items = fake_items_db[commons.skip : commons.skip + commons.limit]
response.update({"items": items})
return response

read_query()
└── Depends(query_or_cookie_extractor)
└── Depends(query_extractor)
async def needy_dependency(fresh_value: Annotated[str, Depends(get_value, use_cache=False)]):
return {"fresh_value": fresh_value}
Depends(get_value)
FastAPI는 기본적으로 같은 요청 내에서 동일한 의존성은 한 번만 실행하고 결과를 캐시(cache)
use_cache=False 를 사용하면 캐시를 쓰지 않아서, 매번 새로 호출됨


| 예시 | 이유 |
|---|---|
get_current_timestamp() | 매번 새 시각 필요 |
get_random_number() | 매번 다른 난수 필요 |
get_fresh_token() | 요청마다 새 인증토큰 필요 |
get_fresh_csrf_nonce() | 폼마다 새로운 보안 토큰 필요 |

@app.get("/items/", dependencies=[Depends(verify_token), Depends(verify_key)])async def verify_token(x_token: Annotated[str, Header()]):
if x_token != "fake-super-secret-token":
raise HTTPException(status_code=400, detail="X-Token header invalid")
app = FastAPI(dependencies=[...])from fastapi import Depends, FastAPI, Header, HTTPException
from typing_extensions import Annotated
async def verify_token(x_token: Annotated[str, Header()]):
if x_token != "fake-super-secret-token":
raise HTTPException(status_code=400, detail="X-Token header invalid")
async def verify_key(x_key: Annotated[str, Header()]):
if x_key != "fake-super-secret-key":
raise HTTPException(status_code=400, detail="X-Key header invalid")
return x_key
app = FastAPI(dependencies=[Depends(verify_token), Depends(verify_key)])
@app.get("/items/")
async def read_items():
return [{"item": "Portal Gun"}, {"item": "Plumbus"}]
@app.get("/users/")
async def read_users():
return [{"username": "Rick"}, {"username": "Morty"}]
app = FastAPI(dependencies=[Depends(verify_token), Depends(verify_key)])app = FastAPI(dependencies=[Depends(auth_check)])

async def get_db(): ... yield db ... 이거 하나로 의존성이 자동으로 되는건 아니고Depends(get_db) 로 연결해야만 의존성 주입이 실제로 동작



with open("./somefile.txt") as f:
contents = f.read()
print(contents)
class MySuperContextManager:
def __init__(self):
self.db = DBSession()
def __enter__(self):
return self.db
def __exit__(self, exc_type, exc_value, traceback):
self.db.close()
async def get_db():
with MySuperContextManager() as db:
yield db


from typing import Annotated
from fastapi import Depends, FastAPI
from fastapi.security import OAuth2PasswordBearer
app = FastAPI()
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") ⭐️
@app.get("/items/")
async def read_items(token: Annotated[str, Depends(oauth2_scheme)]):
return {"token": token}
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")"Bearer <token>"을 찾고<token> 문자열을 리턴oauth2_scheme 는 인스턴스 OAuth2PasswordBearer 이지만 "호출 가능" 변수이기도 함oauth2_scheme(some, parameters) 이렇게 사용도 가능
def fake_decode_token(token):
return User(
username=token + "fakedecoded", email="john@example.com", full_name="John Doe"
)
async def get_current_user(token: str = Depends(oauth2_scheme)):
user = fake_decode_token(token)
return user
OAuth2PasswordRequestFormfrom fastapi.security import OAuth2PasswordRequestFormOAuth2PasswordBearer 와 같이 FastAPI에 대한 특수 클래스가 아님OAuth2PasswordBearer 는 FastAPI가 보안 체계임을 알도록 함OAuth2PasswordRequestForm | 이름 | 등장 위치 | 역할 | 예시 |
|---|---|---|---|
scope (단수) | OAuth2PasswordRequestForm 안에 있는 요청 필드 | 로그인 시 사용자가 요청하는 권한 목록을 공백으로 구분해 전달 | "read write admin" |
scopes (복수) | OAuth2PasswordBearer / OAuth2PasswordRequestForm / Security() 등 FastAPI 보안 구성 내의 속성 | API가 어떤 권한(scope)을 요구하는지를 선언 | scopes={"read": "읽기 권한", "write": "쓰기 권한"} |
POST /token
Content-Type: application/x-www-form-urlencoded
grant_type=password&username=rick&password=secret&scope=⭐️read write
form_data.scope == "read write"
OAuth2 사양은 실제로 password 라는 고정 값이 있는 grant_type 필드를 요구OAuth2PasswordRequestForm 는 grant_type 필드를 강요XOAuth2PasswordRequestFormStrict 를 사용



**user_dict
poetry add pyjwt
alg,typ) sub, exp, scope)| 단계 | 설명 |
|---|---|
| 1. 로그인 요청 | 클라이언트가 /token 에 username/password를 보냄 |
| 2. 서버 검증 | 서버가 사용자 정보 확인 후 JWT 발급 |
| 3. JWT 발급 | JWT를 생성해 클라이언트에게 응답 (access_token) |
| 4. 요청 시 사용 | 클라이언트가 요청할 때 헤더에 JWT 포함 |
| 5. 서버 검증 | 서버는 JWT를 해독해 서명 검증 후 사용자 신원 확인 |
# 로그인(bash)
POST /token
username=rick&password=portalgun
# 서버 응답(json)
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "bearer"
}
# 이후 요청 시(http)
GET /users/me
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
poetry add passlib , poetry add Bcryp(공식문서 추천 알고리즘)from passlib.context import CryptContext
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
# 해싱
hashed = pwd_context.hash("mysecretpassword")
# 검증
pwd_context.verify("mysecretpassword", hashed)
passlib 의 현대적이고 가벼운 대체 라이브러리from pwdlib import PasswordHash
pwd_context = PasswordHash.recommended() # 추천 알고리즘 사용
# 해싱
hashed = pwd_context.hash("mysecretpassword")
# 검증
pwd_context.verify("mysecretpassword", hashed)
| 상황 | 추천 |
|---|---|
| FastAPI 튜토리얼 / 기본 예제 연습 중 | ✅ passlib (공식 문서와 동일) |
| 새로운 프로젝트 + 비동기 코드 중심 | ✅ pwdlib (가볍고 modern) |
| 장기 유지보수 / 기존 레거시 코드 있음 | passlib 유지 |
| 성능·간결성 중시, async 함수 내 사용 | pwdlib 유리 |


from datetime import datetime, timedelta, timezone
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from pydantic import BaseModel
import jwt
from pwdlib import PasswordHash # ✅ pwdlib 임포트
# ===== 설정 =====
SECRET_KEY = "mysecretkey"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
# 앱 인스턴스
app = FastAPI()
# 비밀번호 해싱 도구 초기화
password_context = PasswordHash.recommended()
# ===== 가짜 유저 데이터베이스 =====
# 실제 DB에서는 hashed_password가 저장되어 있음
fake_users_db = {
"rick": {
"username": "rick",
"full_name": "Rick Sanchez",
"email": "rick@example.com",
# "portalgun"을 미리 해싱한 값 (비밀번호 원문은 저장 안 함!)
"hashed_password": password_context.hash("portalgun"),
"disabled": False,
}
}
# ===== 모델 =====
class Token(BaseModel):
access_token: str
token_type: str
class User(BaseModel):
username: str
full_name: str | None = None
email: str | None = None
disabled: bool | None = None
class UserInDB(User):
hashed_password: str
# ===== 유틸 함수 =====
def verify_password(plain_password: str, hashed_password: str):
"""입력받은 평문 비밀번호가 해시와 일치하는지 검증"""
return password_context.verify(plain_password, hashed_password)
def get_user(db, username: str):
"""유저 조회"""
if username in db:
return UserInDB(**db[username])
return None
def authenticate_user(db, username: str, password: str):
"""로그인 검증"""
user = get_user(db, username)
if not user:
return None
if not verify_password(password, user.hashed_password):
return None
return user
def create_access_token(data: dict, expires_delta: timedelta | None = None):
"""JWT 생성"""
to_encode = data.copy()
expire = datetime.now(timezone.utc) + (expires_delta or timedelta(minutes=15))
to_encode.update({"exp": expire})
token = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return token
# ===== 인증 흐름 =====
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
@app.post("/token", response_model=Token)
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
"""로그인 후 JWT 발급"""
user = authenticate_user(fake_users_db, form_data.username, form_data.password)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
)
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
{"sub": user.username}, expires_delta=access_token_expires
)
return {"access_token": access_token, "token_type": "bearer"}
@app.get("/users/me", response_model=User)
async def read_users_me(token: str = Depends(oauth2_scheme)):
"""JWT 검증 → 사용자 정보 반환"""
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username = payload.get("sub")
if username is None:
raise HTTPException(status_code=401, detail="Invalid token")
except jwt.PyJWTError:
raise HTTPException(status_code=401, detail="Invalid token")
user = get_user(fake_users_db, username)
if user is None:
raise HTTPException(status_code=401, detail="User not found")
return user

@app.middleware("http")import time
from fastapi import FastAPI, Request
app = FastAPI()
@app.middleware("http")
async def add_process_time_header(request: Request, call_next):
start_time = time.perf_counter()
response = await call_next(request)
process_time = time.perf_counter() - start_time
response.headers["X-Process-Time"] = str(process_time)
return response
모든 HTTP 요청이 들어올 때마다 자동으로 실행되는 전처리/후처리 로직
@app.middleware("http")
@app.get(),@app.post() 같은 라우트보다 먼저 실행되고,async def add_process_time_header(request: Request, call_next)
call_next(request)가 없으면 라우터에 도달하지 않고, 응답Xstart_time = time.perf_counter()
time.time()보다 perf_counter()는 훨씬 정확해서 처리시간 측정에 자주 사용response = await call_next(request)
[Client Request]
↓
[Middleware 실행 시작]
↓ (1) start_time 기록
↓
(2) call_next(request) → 실제 라우터 실행
↓
(3) 라우터의 response 반환
↓
(4) 처리 시간 계산 및 헤더 추가
↓
[Client Response]

http://127.0.0.1:8000http://localhost:3000"*" ("와일드카드")로 선언하는 것도 가능
CORSMiddleware를 추가해야 함CORSMiddleware 임포트.| 옵션 | 설명 | 기본값 / 예시 |
|---|---|---|
allow_origins | 교차 출처 요청을 허용할 도메인 목록. ['*'] 로 모든 출처 허용 가능. | 기본값: [] |
allow_origin_regex | 허용할 출처를 정규표현식으로 지정. | 예: "https://.*\.example\.org" |
allow_methods | 허용할 HTTP 메서드 목록. ['*'] 로 모든 메서드 허용. | 기본값: ['GET'] |
allow_headers | 요청 시 허용할 헤더 목록. ['*'] 로 모든 헤더 허용. | 기본값: [] (단, Accept, Content-Type 등은 항상 허용) |
allow_credentials | 쿠키·인증정보 포함 요청 허용 여부. True일 경우 allow_origins=['*'] 사용 불가. | 기본값: False |
expose_headers | 브라우저에서 접근 가능한 응답 헤더 목록. | 기본값: [] |
max_age | 브라우저가 CORS 사전 요청(Preflight) 결과를 캐싱하는 시간(초). | 기본값: 600 |
poetry add sqlmodelfrom typing import Annotated
from fastapi import Depends, FastAPI, HTTPException, Query
from sqlmodel import Field, Session, SQLModel, create_engine, select
class Hero(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
name: str = Field(index=True)
age: int | None = Field(default=None, index=True)
secret_name: str
table=TrueField(primary_key=True) : 데이터베이스의 기본키Field(index=True) : SQLModel에 해당 열에 대해 SQL 인덱스를 생성하도록 지시sqlite_file_name = "database.db"
sqlite_url = f"sqlite:///{sqlite_file_name}"
connect_args = {"check_same_thread": False}
engine = create_engine(sqlite_url, connect_args=connect_args)
sqlite_file_name = "database.db"sqlite_url = sqlite:///{sqlite_file_name}connect_args = {"check_same_thread": False}engine = create_engine(sqlite_url, connect_args=connect_args)def create_db_and_tables():
SQLModel.metadata.create_all(engine)
def get_session():
with Session(engine) as session:
yield session
SessionDep = Annotated[Session, Depends(get_session)]
app = FastAPI()
@app.on_event("startup")
def on_startup():
create_db_and_tables()
@app.post("/heroes/")
def create_hero(hero: Hero, session: SessionDep) -> Hero:
session.add(hero)
session.commit()
session.refresh(hero)
return hero
@app.get("/heroes/")
def read_heroes(
session: SessionDep,
offset: int = 0,
limit: Annotated[int, Query(le=100)] = 100,
) -> list[Hero]:
heroes = session.exec(select(Hero).offset(offset).limit(limit)).all() ⭐️
return heroes
@app.get("/heroes/{hero_id}")
def read_hero(hero_id: int, session: SessionDep) -> Hero:
hero = session.get(Hero, hero_id)
if not hero:
raise HTTPException(status_code=404, detail="Hero not found")
return hero
@app.delete("/heroes/{hero_id}")
def delete_hero(hero_id: int, session: SessionDep):
hero = session.get(Hero, hero_id)
if not hero:
raise HTTPException(status_code=404, detail="Hero not found")
session.delete(hero)
session.commit()
return {"ok": True}
class HeroBase(SQLModel):
name: str = Field(index=True)
age: int | None = Field(default=None, index=True)
class Hero(HeroBase, table=True):
id: int | None = Field(default=None, primary_key=True)
secret_name: str
class HeroPublic(HeroBase):
id: int
class HeroCreate(HeroBase):
secret_name: str
class HeroUpdate(HeroBase):
name: str | None = None
age: int | None = None
secret_name: str | None = None
@app.post("/heroes/", response_model=HeroPublic)
def create_hero(hero: HeroCreate, session: SessionDep):
db_hero = Hero.model_validate(hero)
session.add(db_hero)
session.commit()
session.refresh(db_hero)
return db_hero
@app.get("/heroes/", response_model=list[HeroPublic]) ⭐️
def read_heroes(
session: SessionDep,
offset: int = 0,
limit: Annotated[int, Query(le=100)] = 100,
):
heroes = session.exec(select(Hero).offset(offset).limit(limit)).all()
return heroes
response_model=list[HeroPublic] 을 사용하여 데이터가 올바르게 검증되고 직렬화되도록 보장@app.get("/heroes/{hero_id}", response_model=HeroPublic)
def read_hero(hero_id: int, session: SessionDep):
hero = session.get(Hero, hero_id)
if not hero:
raise HTTPException(status_code=404, detail="Hero not found")
return hero
@app.patch("/heroes/{hero_id}", response_model=HeroPublic)
def update_hero(hero_id: int, hero: HeroUpdate, session: SessionDep):
hero_db = session.get(Hero, hero_id)
if not hero_db:
raise HTTPException(status_code=404, detail="Hero not found")
hero_data = hero.model_dump(exclude_unset=True)
hero_db.sqlmodel_update(hero_data)
session.add(hero_db)
session.commit()
session.refresh(hero_db)
return hero_db
@app.delete("/heroes/{hero_id}")
def delete_hero(hero_id: int, session: SessionDep):
hero = session.get(Hero, hero_id)
if not hero:
raise HTTPException(status_code=404, detail="Hero not found")
session.delete(hero)
session.commit()
return {"ok": True}





| 구분 | 설명 |
|---|---|
APIRouter() | 여러 경로를 묶는 미니 앱 |
prefix="/items" | /items 하위 경로로 통합 |
Depends(get_token_header) | 모든 요청에 헤더 토큰 검사 적용 |
read_items() | /items/ 목록 조회 |
read_item() | /items/{item_id} 단일 조회 |
update_item() | /items/{item_id} 수정 (단, plumbus만) |
HTTPException | 요청이 잘못되면 예외 발생 및 HTTP 상태 코드 반환 |
tags, responses | Swagger 문서용 메타데이터 |

| 구분 | 담기는 위치 | 누가 보냄 | 예시 |
|---|---|---|---|
| Header | HTTP 요청 헤더 | 클라이언트 | X-Token: fake-super-secret-token |
| Query Parameter | URL 뒤에 ?token=jessica 형태 | 클라이언트 | /items?token=jessica |
| Body | POST/PUT 요청의 JSON 등 | 클라이언트 | { "title": "Test" } |
| 구문 | 의미 | 경로 기준 |
|---|---|---|
from .module import something | 현재 디렉토리(.) | 같은 폴더 |
from ..module import something | 한 단계 위(..) | 상위 폴더 |
from ...module import something | 두 단계 위(...) | 상위의 상위 폴더 |


