FastAPI에서 GET과 POST 요청이 어떻게 다른지, 조회 API와 데이터 생성 API를 직접 만들며 Request Body, Pydantic, HTTPException, 자동 문서 테스트 흐름까지 초보자 기준으로 정리했습니다.
FastAPI GET, POST 요청 차이는 API를 만들 때 가장 먼저 헷갈리는 부분입니다. 간단히 보면 GET은 데이터를 조회할 때, POST는 새 데이터를 보내거나 생성할 때 주로 사용합니다.
처음에는 @app.get()과 @app.post()가 문법만 다른 것처럼 보일 수 있습니다.
하지만 실제로는 API의 목적이 다릅니다.
- 사용자가 상품 목록을 본다 →
GET - 사용자가 새 상품을 등록한다 →
POST - 게시글 상세 내용을 불러온다 →
GET - 회원가입 정보를 서버로 보낸다 →
POST
이 차이를 이해하면 FastAPI 코드도 훨씬 자연스럽게 읽힙니다.
GET과 POST는 무엇이 다를까?
API 요청은 클라이언트가 서버에 “무엇을 해달라”고 보내는 신호입니다.
여기서 클라이언트는 브라우저, 모바일 앱, 프론트엔드 화면, 다른 서버가 될 수 있습니다.
GET과 POST는 그 요청의 종류입니다.
| 구분 | GET | POST |
|---|---|---|
| 주 용도 | 데이터 조회 | 데이터 생성 또는 전송 |
| 데이터 전달 위치 | URL 경로 또는 쿼리 파라미터 | Request Body |
| 브라우저 주소창 테스트 | 쉬움 | 어렵거나 제한적 |
| FastAPI 문법 | @app.get() |
@app.post() |
| 예시 | 상품 목록 조회 | 새 상품 등록 |
여기서 중요한 점은 GET이 “가볍고”, POST가 “무겁다”는 식으로 외우는 게 아닙니다.
GET은 서버에 있는 데이터를 가져오는 요청, POST는 서버에 데이터를 보내는 요청에 가깝게 이해하면 됩니다.
실습 준비
이 글은 이전 단계에서 FastAPI 설치와 첫 실행까지 끝낸 상태를 기준으로 합니다.
프로젝트 폴더 구조는 아래처럼 두면 됩니다.
fastapi-get-post/
├── .venv/
└── main.py
가상환경이 아직 없다면 먼저 만듭니다.
python -m venv .venv
Windows PowerShell에서는 아래처럼 활성화합니다.
.venv\Scripts\activate.ps1
macOS, Linux에서는 아래 명령어를 사용합니다.
source .venv/bin/activate
FastAPI가 설치되어 있지 않다면 설치합니다.
pip install "fastapi[standard]"
서버 실행은 아래 명령어를 사용할 겁니다.
fastapi dev main.py
fastapi dev main.py는 개발용 실행 명령어입니다. 코드를 수정하고 저장하면 서버가 자동으로 다시 로드되므로, 매번 터미널에서 서버를 껐다 켤 필요는 없습니다.
GET 요청 만들기
먼저 가장 단순한 GET API를 만들어보겠습니다.
main.py 파일에 아래 코드를 작성합니다.
# main.py
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def read_root():
return {"message": "FastAPI GET POST example"}
서버를 실행합니다.
fastapi dev main.py
브라우저에서 아래 주소로 접속합니다.
http://127.0.0.1:8000
응답은 이렇게 나옵니다.
{
"message": "FastAPI GET POST example"
}
여기서 핵심은 이 부분입니다.
@app.get("/")
def read_root():
return {"message": "FastAPI GET POST example"}
@app.get("/")는 / 주소로 GET 요청이 들어왔을 때 바로 아래 함수를 실행하라는 뜻입니다.
브라우저 주소창에 URL을 입력해서 접속하는 행동도 기본적으로 GET 요청에 가깝습니다. 그래서 GET API는 주소창에서 바로 테스트하기 쉽습니다.
상품 목록을 GET으로 조회하기
이번에는 조금 더 API다운 예제를 만들어보겠습니다.
상품 목록을 조회하는 API입니다.
main.py를 아래처럼 수정합니다.
# main.py
from fastapi import FastAPI
app = FastAPI()
items = [
{"id": 1, "name": "keyboard", "price": 30000},
{"id": 2, "name": "mouse", "price": 15000},
]
@app.get("/")
def read_root():
return {"message": "FastAPI GET POST example"}
@app.get("/items")
def read_items():
return {"items": items}
브라우저에서 아래 주소로 접속합니다.
http://127.0.0.1:8000/items
응답은 이렇게 나옵니다.
{
"items": [
{
"id": 1,
"name": "keyboard",
"price": 30000
},
{
"id": 2,
"name": "mouse",
"price": 15000
}
]
}
이 API는 서버에 있는 items 목록을 가져옵니다.
그래서 GET /items가 자연스럽습니다.
여기서 items는 임시 데이터입니다. 실제 서비스라면 데이터베이스에 저장된 상품 목록을 가져오겠지만, 처음에는 리스트로 흐름만 이해해도 충분합니다.
Path Parameter로 특정 상품 조회하기
상품 전체 목록이 아니라 특정 상품 하나만 조회하고 싶을 수도 있습니다.
예를 들어 아래 주소처럼 상품 ID를 URL에 넣는 방식입니다.
http://127.0.0.1:8000/items/1
이런 값을 Path Parameter라고 합니다. FastAPI 공식 문서에서도 /items/{item_id} 같은 형식으로 path parameter를 선언하는 방식을 안내합니다.
main.py에 아래 코드를 추가합니다.
# main.py
from fastapi import FastAPI, HTTPException
app = FastAPI()
items = [
{"id": 1, "name": "keyboard", "price": 30000},
{"id": 2, "name": "mouse", "price": 15000},
]
@app.get("/")
def read_root():
return {"message": "FastAPI GET POST example"}
@app.get("/items")
def read_items():
return {"items": items}
@app.get("/items/{item_id}")
def read_item(item_id: int):
for item in items:
if item["id"] == item_id:
return item
raise HTTPException(status_code=404, detail="Item not found")
브라우저에서 아래 주소로 접속합니다.
http://127.0.0.1:8000/items/1
응답은 이렇게 나옵니다.
{
"id": 1,
"name": "keyboard",
"price": 30000
}
이번에는 2번 상품을 조회해봅니다.
http://127.0.0.1:8000/items/2
응답은 이렇게 바뀝니다.
{
"id": 2,
"name": "mouse",
"price": 15000
}
없는 상품 ID를 조회하면 어떻게 될까요?
http://127.0.0.1:8000/items/999
이 경우에는 아래 코드가 실행됩니다.
raise HTTPException(status_code=404, detail="Item not found")
HTTPException은 API에서 에러 응답을 명확하게 내려줄 때 사용합니다. 여기서는 상품을 찾지 못했으므로 404 Not Found 상태 코드를 반환합니다.
이전처럼 단순히 {"message": "Item not found"}를 반환할 수도 있습니다. 하지만 그러면 실제로는 상품을 못 찾았는데도 HTTP 상태 코드는 200 OK가 될 수 있습니다. API를 설계할 때는 이런 부분을 구분해주는 편이 좋습니다.
여기서 봐야 할 또 다른 부분은 item_id: int입니다.
def read_item(item_id: int):
FastAPI는 URL로 들어온 값이 정수인지 확인합니다.
예를 들어 /items/abc처럼 숫자가 아닌 값을 넣으면 FastAPI는 item_id를 정수로 바꿀 수 없다고 판단하고 422 검증 에러를 반환합니다.
응답 형식은 FastAPI와 Pydantic 버전에 따라 조금 다를 수 있지만, 대략 아래처럼 item_id 위치에서 정수 변환에 실패했다는 내용이 포함됩니다.
{
"detail": [
{
"loc": ["path", "item_id"],
"msg": "Input should be a valid integer"
}
]
}
이게 FastAPI의 장점 중 하나입니다. Python 타입 힌트를 적으면 요청 데이터 검증과 문서화에 함께 활용됩니다.
Query Parameter로 검색 조건 받기
GET 요청에서는 URL 뒤에 조건을 붙여서 데이터를 조회할 수도 있습니다.
예를 들어 아래 주소를 봅니다.
http://127.0.0.1:8000/search?keyword=key
?keyword=key 부분이 Query Parameter입니다.
검색어, 페이지 번호, 정렬 조건처럼 “조회 조건”을 보낼 때 자주 사용합니다. FastAPI 공식 문서에서도 함수 인자 중 path에 포함되지 않은 단순 타입 값은 query parameter로 해석된다고 설명합니다.
main.py를 아래처럼 수정합니다.
# main.py
from fastapi import FastAPI, HTTPException
app = FastAPI()
items = [
{"id": 1, "name": "keyboard", "price": 30000},
{"id": 2, "name": "mouse", "price": 15000},
]
@app.get("/")
def read_root():
return {"message": "FastAPI GET POST example"}
@app.get("/items")
def read_items():
return {"items": items}
@app.get("/items/{item_id}")
def read_item(item_id: int):
for item in items:
if item["id"] == item_id:
return item
raise HTTPException(status_code=404, detail="Item not found")
@app.get("/search")
def search_items(keyword: str = ""):
result = []
for item in items:
if keyword.lower() in item["name"].lower():
result.append(item)
return {"keyword": keyword, "result": result}
브라우저에서 아래 주소로 접속합니다.
http://127.0.0.1:8000/search?keyword=key
응답은 이렇게 나옵니다.
{
"keyword": "key",
"result": [
{
"id": 1,
"name": "keyboard",
"price": 30000
}
]
}
keyword 값을 바꿔보면 결과도 바뀝니다.
http://127.0.0.1:8000/search?keyword=mouse
이렇게 GET 요청은 데이터를 조회하면서 조건을 붙이는 데 잘 어울립니다.
POST 요청 만들기
이제 POST 요청을 만들어보겠습니다.
POST는 보통 클라이언트가 서버에 데이터를 보낼 때 사용합니다. 예를 들어 새 상품 등록, 회원가입, 게시글 작성 같은 작업입니다.
GET은 주소창에서 쉽게 테스트할 수 있었지만, POST는 보통 Request Body에 JSON 데이터를 담아 보냅니다. 그래서 브라우저 주소창만으로는 테스트하기 어렵고, FastAPI의 /docs 화면이나 Postman 같은 도구를 사용합니다.
FastAPI에서는 Request Body를 받을 때 Pydantic의 BaseModel을 자주 사용합니다. 공식 문서에서도 Pydantic 모델을 함수 인자로 선언하면 해당 값이 request body로 처리된다고 안내합니다.
main.py를 아래처럼 수정합니다.
# main.py
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
price: int
items = [
{"id": 1, "name": "keyboard", "price": 30000},
{"id": 2, "name": "mouse", "price": 15000},
]
@app.get("/")
def read_root():
return {"message": "FastAPI GET POST example"}
@app.get("/items")
def read_items():
return {"items": items}
@app.get("/items/{item_id}")
def read_item(item_id: int):
for item in items:
if item["id"] == item_id:
return item
raise HTTPException(status_code=404, detail="Item not found")
@app.post("/items")
def create_item(item: Item):
new_item = {
"id": len(items) + 1,
"name": item.name,
"price": item.price,
}
items.append(new_item)
return new_item
새로 추가된 부분은 두 군데입니다.
먼저 요청으로 받을 데이터 형식을 정의합니다.
class Item(BaseModel):
name: str
price: int
이 코드는 “상품을 등록할 때 name은 문자열, price는 정수로 받겠다”는 뜻입니다.
다음은 POST API입니다.
@app.post("/items")
def create_item(item: Item):
new_item = {
"id": len(items) + 1,
"name": item.name,
"price": item.price,
}
items.append(new_item)
return new_item
@app.post("/items")는 /items 주소로 POST 요청이 들어왔을 때 실행됩니다.
GET으로 /items에 접근하면 상품 목록을 조회하고, POST로 /items에 요청하면 새 상품을 추가하는 구조입니다.
주소는 같아도 요청 방식이 다르면 다른 API처럼 동작할 수 있습니다.
/docs에서 POST 요청 테스트하기
POST 요청은 FastAPI 자동 문서에서 테스트하는 편이 가장 쉽습니다.
서버가 실행 중인 상태에서 아래 주소로 들어갑니다.
http://127.0.0.1:8000/docs
화면에서 POST /items 항목을 찾습니다.
아래 순서대로 진행합니다.
- **
POST /items** 항목을 클릭합니다. - **
Try it out** 버튼을 누릅니다. - Request body 영역에 JSON 데이터를 입력합니다.
- **
Execute** 버튼을 누릅니다. - 하단의 Response body에서 응답 결과를 확인합니다.
Request body에는 아래처럼 입력합니다.
{
"name": "monitor",
"price": 200000
}
성공하면 응답은 아래처럼 나옵니다.
{
"id": 3,
"name": "monitor",
"price": 200000
}
이제 다시 GET /items를 실행해보면 새 상품이 목록에 추가된 것을 볼 수 있습니다.
{
"items": [
{
"id": 1,
"name": "keyboard",
"price": 30000
},
{
"id": 2,
"name": "mouse",
"price": 15000
},
{
"id": 3,
"name": "monitor",
"price": 200000
}
]
}
단, 이 예제는 메모리 안의 리스트에 데이터를 추가하는 방식입니다.
서버를 껐다가 다시 켜면 추가한 데이터는 사라집니다. 실제 서비스에서는 이런 데이터를 데이터베이스에 저장해야 합니다.
GET과 POST를 같은 주소로 쓰는 이유
처음 보면 헷갈릴 수 있습니다.
@app.get("/items")
def read_items():
return {"items": items}
@app.post("/items")
def create_item(item: Item):
...
둘 다 주소는 /items입니다.
하지만 하나는 GET이고, 하나는 POST입니다.
이 구조는 실제 API에서 자연스럽게 쓰입니다.
GET /items→ 상품 목록 조회POST /items→ 새 상품 등록GET /items/1→ 1번 상품 조회
주소는 리소스를 나타내고, HTTP 메서드는 행동을 나타낸다고 보면 됩니다.
/items는 “상품들”이라는 자원입니다.
그 상품들을 조회하면 GET이고, 상품을 새로 추가하면 POST입니다.
GET으로 데이터를 보내면 안 될까?
GET도 query parameter를 통해 값을 보낼 수 있습니다.
예를 들어 검색어는 이렇게 보낼 수 있습니다.
/search?keyword=keyboard
하지만 상품 등록처럼 구조가 있는 데이터는 GET보다는 POST가 어울립니다.
{
"name": "monitor",
"price": 200000
}
이런 데이터는 URL에 붙이기보다 Request Body에 담아 보내는 편이 자연스럽습니다.
특히 회원가입, 게시글 작성, 주문 생성처럼 서버 상태가 바뀌는 작업은 GET보다 POST로 설계하는 게 일반적입니다.
여기서 오해하기 쉬운 부분이 있습니다.
POST Body에 넣는다고 해서 자동으로 보안이 보장되는 것은 아닙니다. URL에 그대로 보이지 않을 뿐입니다. 개발자 도구의 Network 탭을 보면 POST Body에 담긴 값도 확인할 수 있습니다.
실제 서비스에서 민감한 데이터를 다룬다면 HTTPS, 인증, 권한 확인 같은 보안 처리가 필요합니다.
잘못된 데이터가 들어오면 어떻게 될까?
Item 모델은 name을 문자열, price를 정수로 받도록 정의했습니다.
class Item(BaseModel):
name: str
price: int
그런데 /docs에서 아래처럼 잘못된 데이터를 보내봅니다.
{
"name": "monitor",
"price": "비쌈"
}
그러면 FastAPI는 요청을 그대로 처리하지 않고 검증 에러를 반환합니다.
price는 정수여야 하는데 문자열이 들어왔기 때문입니다.
이 부분이 FastAPI와 Pydantic을 함께 사용할 때 편한 지점입니다. 요청 데이터의 형식을 코드에 적어두면 FastAPI가 자동으로 검증하고, API 문서에도 해당 구조를 보여줍니다.
전체 코드
지금까지 작성한 전체 코드는 아래와 같습니다.
# main.py
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
price: int
items = [
{"id": 1, "name": "keyboard", "price": 30000},
{"id": 2, "name": "mouse", "price": 15000},
]
@app.get("/")
def read_root():
return {"message": "FastAPI GET POST example"}
@app.get("/items")
def read_items():
return {"items": items}
@app.get("/items/{item_id}")
def read_item(item_id: int):
for item in items:
if item["id"] == item_id:
return item
raise HTTPException(status_code=404, detail="Item not found")
@app.get("/search")
def search_items(keyword: str = ""):
result = []
for item in items:
if keyword.lower() in item["name"].lower():
result.append(item)
return {"keyword": keyword, "result": result}
@app.post("/items")
def create_item(item: Item):
new_item = {
"id": len(items) + 1,
"name": item.name,
"price": item.price,
}
items.append(new_item)
return new_item
서버 실행 명령어는 아래와 같습니다.
fastapi dev main.py
자동 문서 화면은 아래 주소에서 확인합니다.
http://127.0.0.1:8000/docs
자주 헷갈리는 부분
GET과 POST는 주소가 달라야 하나요?
꼭 그렇지는 않습니다.
GET /items와 POST /items처럼 주소가 같아도 됩니다.
요청 방식이 다르기 때문에 FastAPI는 서로 다른 함수로 처리합니다.
POST 요청은 브라우저 주소창에서 테스트할 수 있나요?
일반적인 방식으로는 어렵습니다.
주소창에 URL을 입력하는 것은 보통 GET 요청입니다. POST 요청은 /docs, Postman, curl, 프론트엔드 코드 등을 사용해서 테스트하는 편이 좋습니다.
Request Body와 Query Parameter는 뭐가 다른가요?
Query Parameter는 URL에 붙는 값입니다.
/search?keyword=keyboard
Request Body는 요청 안에 담겨서 전달되는 데이터입니다.
{
"name": "keyboard",
"price": 30000
}
검색, 필터링, 페이지 번호처럼 간단한 조회 조건은 Query Parameter가 어울립니다.
새 데이터를 생성하거나 구조가 있는 데이터를 보낼 때는 Request Body가 더 자연스럽습니다.
BaseModel은 왜 필요한가요?
BaseModel은 요청 데이터의 형태를 정의하는 데 사용합니다.
class Item(BaseModel):
name: str
price: int
이렇게 작성하면 FastAPI가 요청 데이터를 검증하고, /docs 문서에도 어떤 JSON을 보내야 하는지 표시해줍니다.
HTTPException은 왜 사용하나요?
API에서 에러 상황을 HTTP 상태 코드로 명확하게 알려주기 위해 사용합니다.
예를 들어 없는 상품을 조회했을 때는 단순 메시지보다 404 Not Found를 반환하는 편이 더 자연스럽습니다.
raise HTTPException(status_code=404, detail="Item not found")
FastAPI에서는 HTTPException을 return하는 것이 아니라 raise해서 사용합니다.
처음에는 이렇게 구분하면 된다
FastAPI에서 GET과 POST를 처음 배울 때는 너무 복잡하게 외울 필요가 없습니다.
아래 기준만 잡아도 대부분의 입문 예제는 이해할 수 있습니다.
GET → 서버에서 데이터를 가져온다
POST → 서버로 데이터를 보낸다
상품 목록을 본다면 GET입니다.
새 상품을 등록한다면 POST입니다.
검색어처럼 간단한 조건은 URL의 Query Parameter로 보내고, 상품 정보처럼 구조가 있는 데이터는 POST의 Request Body로 보냅니다.
없는 상품처럼 요청 결과를 정상으로 처리하기 어려운 경우에는 HTTPException으로 적절한 상태 코드를 반환할 수 있습니다.
이 흐름이 잡히면 다음 단계로는 PUT, PATCH, DELETE 요청을 이어서 배우기 좋습니다. 그때부터는 단순 조회와 생성뿐 아니라 수정, 일부 수정, 삭제까지 API 구조가 조금 더 실제 서비스에 가까워집니다.
'개발 > FastAPI' 카테고리의 다른 글
| FastAPI Swagger 문서 자동 생성, `/docs` 화면이 만들어지는 원리 (0) | 2026.05.26 |
|---|---|
| FastAPI Pydantic 모델 기초: 요청 데이터를 검증하는 방법 (0) | 2026.05.26 |
| FastAPI Path Parameter와 Query Parameter 차이 정리 (0) | 2026.05.24 |
| FastAPI 설치와 시작하기: 첫 API 만들고 문서 확인까지 (0) | 2026.05.24 |
| FastAPI란? Flask·Django 차이와 파이썬 백엔드 선택 기준 (0) | 2026.05.23 |