response model은 API route 경로가 반환하는 데이터의 template 역할을 하며, 서버에 전달된 요청을 기준으로 적절한 응답을 렌더링하기 위해 pydantic
을 사용한다.
오류 처리는 application에서 발생하는 오류를 처리하는 로직과 방법을 의미한다. 오류 처리에는 적절한 오류 상태 코드와 오류 메시지가 포함된다.
응답은 HTTP 메서드를 통해 API와 상호작용하며, API로부터 받은 결과를 가리킨다. 응답은 header와 body로 이루어져있으며, http status code를 포함한다.
응답 헤더는 요청 상태 및 응답 바디 전달을 안내하는 정보로 구성된다. 대표적으로 Content-Type
이 있으며 콘텐츠 유형이 무엇인지 클라이언트에게 알려주는 역할을 한다.
응답 바디는 서버가 클라이언트에게 반환하는 데이터이다. Content-Type
헤더에 의해 응답 바디의 형식이 결정되며 대표적인 예로 application/json
이 있다.
상태 코드는 서버가 반환한 응답에 포함되는 짧은 고유 코드이다. 클라이언트가 보낸 요청에 대한 응답 상태를 보내는 것인데, 숫별로 다음의 의미를 가진다.
상태 코드의 첫 번째 숫자는 상태 그룹을 의미한다. 200은 요청이 정상적으로 성공했다는 것을 의미하고, 400은 클라이언트의 요청이 잘못되었다는 것을, 500은 서버측의 문제가 발생하여 요청을 처리하지 못했다는 것을 의미한다.
이전에 사용했던 코드를 정리하면 다음과 같다.
from fastapi import FastAPI
from todo import todo_router
app = FastAPI()
@app.get('/')
async def welcome() -> dict:
return {"message": "Hello World"}
app.include_router(router=todo_router)
from fastapi import APIRouter, Path
from model import Todo, TodoItem
todo_router = APIRouter()
todo_list = []
@todo_router.post("/todo")
async def add_todo(todo: Todo) -> dict:
todo_list.append(todo)
return {"message": "Todo added successfully"}
@todo_router.get("/todo")
async def retrieve_todos() -> dict:
return {"todos": todo_list}
@todo_router.get("/todo/{todo_id}")
async def get_single_todo(todo_id: int = Path(..., title="The ID of the todo to retrieve.")) -> dict:
for todo in todo_list:
if todo.id == todo_id:
return {
"todo": todo
}
return {
"message": "Todo with supplied ID dosen't exist."
}
@todo_router.put("/todo/{todo_id}")
async def update_todo(todo_data: TodoItem, todo_id: int = Path(..., title="The Id of the todo to be updated")) -> dict:
for todo in todo_list:
if todo.id == todo_id:
todo.item = todo_data.item
return {
"message": "Todo updated successfully"
}
return {
"message": "Todo with supplied ID doesn't exist."
}
@todo_router.delete('/todo/{todo_id}')
async def delete_single_todo(todo_id: int) -> dict:
for index in range(len(todo_list)):
todo = todo_list[index]
if todo.id == todo_id:
todo_list.pop(index)
return {
"message": "Todo deleted successfully"
}
return {
"message": "Todo with supplied ID dosen't exist"
}
@todo_router.delete("/todo")
async def delete_all_todo() -> dict:
todo_list.clear()
return {
"message": "Todos deleted successfully"
}
from pydantic import BaseModel
class Todo(BaseModel):
id: int
item: str
class Config:
schema_extra = {
"example": {
"id": 1,
"item": "Example Schema!"
}
}
class TodoItem(BaseModel):
item: str
class Config:
schema_extra = {
"example": {
"item": "Read the next chapter of the book"
}
}
응답 모델도 pydantic
을 사용해 작성하지만 목적이 다르다.
다음의 /todo
경로의 요청은 todo_list
를 불러와준다.
@todo_router.get("/todo")
async def retrieve_todos() -> dict:
return {"todos": todo_list}
이 route는 todo 배열에 있는 모든 값을 반환한다. TodoItems
라는 model을 만들어 response model로 두도록 하자.
모든 todo
를 추출해서 배열로 반환하는 라우트를 ID없이 todo 아이템만 반환하도록 변경해보자.
먼저 model.py
에 다음과 같이 새로운 모델을 추가하자.
model.py
from pydantic import BaseModel
from typing import List
...
class TodoItems(BaseModel):
todos: List[TodoItem]
class Config:
schema_extra = {
"example": {
"todos": [
{
"item": "Example schema1"
},
{
"item": "Example schema2"
}
]
}
}
TodoItems
라는 새로운 모델을 정의해서 TodoItem
모델에 정의된 변수 목록을 반환한다. todo.py
에 있는 라우트에 다음과 같이 응답 모델을 추가해보도록하자.
todo.py
...
from fastapi import APIRouter, Path
from model import Todo, TodoItem, TodoItems
...
@todo_router.get("/todo", response_model=TodoItems)
async def retrieve_todos() -> dict:
return {"todos": todo_list}
...
@todo_router.get
부분에 response_model
로 TodoItems
가 추가된 것을 확인하도록 하자.
이제 어플리케이션을 실행해보도록 하자.
uvicorn api:app --host=127.0.0.1 --port=8080 --reload
어플리케이션이 실행되었다면 todo
를 추가해주도록 하자.
curl -X 'POST' 'localhost:8080/todo' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"id": 1,
"item": "This todo will be retrieved without exposing my ID!"
}'
이제 todo
list를 가져와 보자.
curl localhost:8080/todo
{"todos":[{"item":"This todo will be retrieved without exposing my ID!"}]}
잘 도착하는 것을 볼 수 있다.
만약 응답 모델을 따르지 않으면 어떻게되는가?? fastapi에서는 이를 오류로 보고 있어서 서버측 에러를 반환한다.
...
@todo_router.get("/todo", response_model=TodoItems)
async def retrieve_todos() -> dict:
return {"fake": "fake"}
...
다음과 같이 의도적으로 응답 모델과 다른 응답을 전달하려고 한다면 요청 시에 다음과 같은 에러가 발생한다.
curl localhost:8080/todo -v
* Trying 127.0.0.1:8080...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /todo HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.79.1
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 500 Internal Server Error
< date: Wed, 31 May 2023 15:11:33 GMT
< server: uvicorn
< content-length: 21
< content-type: text/plain; charset=utf-8
<
* Connection #0 to host localhost left intact
Internal Server Error%
Internal Server Error
와 500 status code가 온 것을 확인할 수 있다.
클라이언트에게 오류 로그를 응답으로 전달하거나, Exception 내용을 전달하는 것은 좋은 응답이아니다.
존재하지 않는 리소스나 없는 페이지에 접근하는 경우 요청 시 오류가 발생하며 서버 자체에서 오류가 발생하기도 한다. FastAPI
에서 오류는 FastAPI
의 HTTPException
클래스를 사용해 exception을 발생시켜 처리한다.
HTTPException
클래스는 다음 3가지 인수를 받는다.
현재 path parameter로 todo
라우트에 대해 요청할 때 todo_id
가 todo list에 없는 경우를 요청하면 message
를 반환한다.
@todo_router.get("/todo/{todo_id}")
async def get_single_todo(todo_id: int = Path(..., title="The ID of the todo to retrieve.")) -> dict:
for todo in todo_list:
if todo.id == todo_id:
return {
"todo": todo
}
return {
"message": "Todo with supplied ID dosen't exist."
}
이렇게 되면 status code도 200이 전달되기 때문에 올바른 응답이 아니다.
이 경우 HTTPException
을 사용해 예외를 발생시키도록 수정해보자. HTTPException
을 사용하면 적절한 오류 코드를 응답에 포함시킬 수 있다.
from fastapi import APIRouter, Path, HTTPException, status
...
@todo_router.get("/todo/{todo_id}")
async def get_single_todo(todo_id: int = Path(..., title="The ID of the todo to retrieve.")) -> dict:
for todo in todo_list:
if todo.id == todo_id:
return {
"todo": todo
}
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Totd with supplied ID doesn't exist"
)
@todo_router.put("/todo/{todo_id}")
async def update_todo(todo_data: TodoItem, todo_id: int = Path(..., title="The Id of the todo to be updated")) -> dict:
for todo in todo_list:
if todo.id == todo_id:
todo.item = todo_data.item
return {
"message": "Todo updated successfully"
}
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Totd with supplied ID doesn't exist"
)
@todo_router.delete('/todo/{todo_id}')
async def delete_single_todo(todo_id: int) -> dict:
for index in range(len(todo_list)):
todo = todo_list[index]
if todo.id == todo_id:
todo_list.pop(index)
return {
"message": "Todo deleted successfully"
}
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Totd with supplied ID doesn't exist"
)
@todo_router.delete("/todo")
async def delete_all_todo() -> dict:
todo_list.clear()
return {
"message": "Todos deleted successfully"
}
이제 존재하지 않는 todo id를 요청하면 어떤 응답이 전달되는 지 확인해보도록 하자.
curl localhost:8080/todo/2 -v
* Trying 127.0.0.1:8080...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /todo/2 HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.79.1
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 404 Not Found
< date: Wed, 31 May 2023 15:30:15 GMT
< server: uvicorn
< content-length: 48
< content-type: application/json
<
* Connection #0 to host localhost left intact
{"detail":"Totd with supplied ID doesn't exist"}
404
Not Found status code아 detail
정보가 제대로 전달된 것을 볼 수 있다.
마지막으로 /todo
에서 post
로 요청할 때, 성공 시 응답 코드를 201 Created로 전달하도록 변경해보자. 기본적으로 fastapi는 응답으로 200을 반환하기 때문에 수정해주어야 한다.
...
@todo_router.post("/todo", status_code=status.HTTP_201_CREATED)
async def add_todo(todo: Todo) -> dict:
todo_list.append(todo)
return {"message": "Todo added successfully"}
...
post
요청을 보내보도록 하자.
curl -X 'POST' 'localhost:8080/todo' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"id": 2,
"item": "hello world!"
}' -v
* Trying 127.0.0.1:8080...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> POST /todo HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.79.1
> accept: application/json
> Content-Type: application/json
> Content-Length: 44
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 201 Created
< date: Wed, 31 May 2023 15:34:46 GMT
< server: uvicorn
< content-length: 37
< content-type: application/json
<
* Connection #0 to host localhost left intact
{"message":"Todo added successfully"
201 Created
응답이 잘 전달된 것을 확인할 수 있다.
이 처럼 APIRouter
의 데코레이터 부분을 통해서 기본적으로 응답에 성공했을 때의 status code를 설정할 수 있으며, HTTPException
으로 http에 관련된 에러가 발생했을 때 오류와 함께 status code
를 전달할 수 있다.