FastAPI [2] – 응답 모델, 오류 처리, Jinja를 활용한 템플릿팅

Response model(응답 모델)

  • 응답 모델은 API 라우트 경로가 반환하는 데이터의 템플릿 역할을 한다.
  • 애플리케이션은 클라이언트에 반환할 응답을 렌더링하기 위해 pydantic을 사용한다.
class TodoItems(BaseModel):
    todos: List[TodoItem]

    class Config:
        schema_extra = {
            "example": {
                "todos": [{"item": "Example schema 1!"}, {"item": "Example schema 2!"}]
            }
        }

Error handling(오류 처리)

  • 오류 처리는 애플리케이션에서 발생하는 오류를 처리하는 로직과 방법을 의미하며, 오류를 반환할 때는 오류 상태 코드와 오류 메시지를 포함시켜야 한다.
  • FastAPI에서는 HTTPException 클래스를 사용해 예외를 발생시켜 처리한다.
  • HTTPException 인수
    • status_code: 오류 상태 코드
    • detail: 오류 메시지
    • headers: 헤더를 요구하는 응답을 위한 선택적 인수
@todo_router.post("/todo", status_code=201)
async def add_todo(todo: Todo) -> dict:
    todo_list.append(todo)
    return {"message": "Todo added successfully."}
  • 위 코드는 데코레이터 함수에 status_code를 추가해서 기본 응답 코드(200)을 201로 변경하도록 하는 방법이다.
@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="Todo with supplied ID doesn't exist.",
    )
  • 위 코드는 HTTPException 클래스로 예외를 발생시켜 에러 상태 코드와 메시지를 전달하는 방법이다.

Templating(템플릿팅)

  • 템플릿팅은 API가 보낸 다양한 형식의 데이터를 화면에 표시하는 프로세스다.
  • 템플릿은 웹 애플리케이션상 프론트엔드 컴포넌트처럼 처리된다.
  • Jinja는 파이썬으로 작성된 템플릿팅 엔진으로, API 응답을 쉽게 렌더링할 수 있게 해준다.
$ pip install jinja2 python-multipart
  • Jinja는 중괄호 {}를 사용해서 html, 텍스트 등을 표현식 및 구문과 구분한다.
    • {{}} 구문은 변수 블록(variable block)이라고 하며 이 안에 변수를 지정한다.
    • {% … %} 구문은 if/else, 반복, 매크로 같은 구조(처리)를 제어할 때 사용한다.
    • {# … #} 구문은 주석을 기입할 때 사용한다.
    • 필터 |는 가장 중요한 요소이며 특정 함수를 실행할 수 있게 해준다.
  • Jinja 템플릿 문법 가이드 문서

home.html – 부모 템플릿

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta content="IE=edge" http-equiv="X-UA-Compatible">
    <meta content="width=device-width, initial-scale=1.0" name="viewport">
    <title>Packt Todo Application</title>
    <link crossorigin="anonymous" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css"
          integrity="sha384-9gVQ4dYFwwWSjIDZnLEWnxCjeSWFphJiwGPXr1jddIhOegiu1FwO5qRGvFXOdJZ4" rel="stylesheet">
    <link crossorigin="anonymous" href="https://use.fontawesome.com/releases/v5.0.10/css/all.css"
          integrity="sha384-+d0P83n9kaQMCwj8F4RJB66tzIwOKmrdb46+porD/OvrJ+37WqIM7UoBtwHO6Nlg" rel="stylesheet">
</head>
<body>
<header>
    <nav class="navar">
        <div class="container-fluid">
            <center>
                <h1>Packt Todo Application</h1>
            </center>
        </div>
    </nav>
</header>
<div class="container-fluid">
    {% block todo_container %}{% endblock %}
</div>
</body>
</html>

todo.html – 자식 템플릿(extends를 통해 home.html 상속)

{% extends "home.html" %}

{% block todo_container %}
<main class="container">
    <hr>
    <section class="container-fluid">
        <form method="post">
            <div class="col-auto">
                <div class="input-group mb-3">
                    <input aria-describedby="button-addon2" aria-label="Add a todo" class="form-control" name="item"
                           placeholder="Purchase Packt's Python workshop course" type="text"
                           value="{{ item }}"/>
                    <button class="btn btn-outline-primary" data-mdb-ripple-color="dark" id="button-addon2"
                            type="submit">
                        Add Todo
                    </button>
                </div>
            </div>
        </form>
    </section>
    {% if todo %}
    <article class="card container-fluid">
        <br/>
        <h4>Todo ID: {{ todo.id }} </h4>
        <p>
            <strong>
                Item: {{ todo.item }}
            </strong>
        </p>
    </article>
    {% else %}
    <section class="container-fluid">
        <h2 align="center">Todos</h2>
        <br>
        <div class="card">
            <ul class="list-group list-group-flush">
                {% for todo in todos %}
                <li class="list-group-item">
                    {{ loop.index }}. <a href="/todo/{{ loop.index }}"> {{ todo.item }} </a>
                </li>
                {% endfor %}
            </ul>
        </div>
        {% endif %}
    </section>
</main>
{% endblock %}
  • html 템플릿 파일의 예시이다. 루트 위치 templates 디렉토리 안에 템플릿 파일을 작성한다.
  • 부모 템플릿의 todo_container 변수 블록에 자식 템플릿의 todo_container 블록에 담긴 콘텐츠를 넣어서 반환할 수 있다.
  • todo 템플릿에서는 {% if / else %} 구문을 통해 todo 변수가 전달 되었을 때만 todo 정보가 표시되고, 없으면 else 블록에 있는 콘텐츠를 표시하도록 작성했다.
class Todo(BaseModel):
    id: Optional[int] = Field(None)
    item: str

    @classmethod
    def as_form(cls, item: str = Form(...)):
        return cls(item=item)

    class Config:
        schema_extra = {"example": {"id": 1, "item": "Example schema!"}}
  • 모델에서는 as_form이라는 클래스 메서드를 추가해준다.
from fastapi import APIRouter, Path, HTTPException, status, Request, Depends
from fastapi.templating import Jinja2Templates

from model import Todo, TodoItem, TodoItems

todo_router = APIRouter()

todo_list = []

templates = Jinja2Templates(directory="templates/")


@todo_router.post("/todo", status_code=201)
async def add_todo(request: Request, todo: Todo = Depends(Todo.as_form)):
    todo.id = len(todo_list) + 1
    todo_list.append(todo)
    return templates.TemplateResponse(
        "todo.html", {"request": request, "todos": todo_list}
    )
  • 위 코드는 Jinja가 templates 폴더에 있는 todo.html 템플릿 파일을 사용해서 응답을 반환하도록 하는 코드이다.
  • home.html의 템플릿을 상속한 todo.html이 화면에 뿌려지게 된다.

Leave a Reply

Your email address will not be published. Required fields are marked *