0Pricing
FastAPI Backend Development Bootcamp · 강의

안정적인 응답 봉투 설계

메타데이터, 링크 및 오류 형식을 표준화해 API 소비자가 예측 가능하고 하위 호환되는 데이터를 받도록 합니다.

안정적인 응답 봉투 설계은(는) CoddyKit의 무료 FastAPI Backend Development Bootcamp 강의입니다. 이것은 4개 중 4번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 FastAPI Backend Development Bootcamp 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. FastAPI Backend Development Bootcamp 강의에는 총 4개의 강의가 포함되어 있습니다.

이 강의의 일부는 아직 번역되지 않았으며 영어로 표시됩니다.

Why Response Envelopes Matter

A response envelope is a consistent outer shape that wraps every payload your API returns. Instead of returning bare data on one route and a different ad-hoc structure on another, you commit to ONE predictable skeleton.

Consumers benefit because they can:

  • Write one parser that works across all endpoints
  • Always know where to find data, meta, and errors
  • Rely on the shape staying backward-compatible as you add fields

In this lesson you will design stable envelopes for success, pagination, and errors in FastAPI.

The Bare-Data Anti-Pattern

The most common mistake is returning naked data and changing its shape per route. Here a list endpoint returns an array while a detail endpoint returns an object, and there is nowhere to attach pagination or warnings.

When you later need to add a total count, you must break the array contract or bolt on inconsistent fields. A pure dict has no room to grow safely.

# Anti-pattern: inconsistent, no room to grow
def list_users():
    return [{"id": 1, "name": "Ada"}]  # bare array

def get_user(user_id):
    return {"id": user_id, "name": "Ada"}  # bare object

# Later you need pagination... where does total go?
print(list_users())
print(get_user(1))

A Minimal Success Envelope

Start with a generic envelope that carries the real payload under data and leaves slots for meta and errors. Using a generic type keeps it reusable for any payload.

Pydantic generics let one model serve every endpoint while preserving the inner schema for OpenAPI docs.

from typing import Generic, TypeVar, Optional, Any
from pydantic import BaseModel

T = TypeVar("T")

class Envelope(BaseModel, Generic[T]):
    data: Optional[T] = None
    meta: dict[str, Any] = {}
    errors: list[dict[str, Any]] = []

class User(BaseModel):
    id: int
    name: str

env = Envelope[User](data=User(id=1, name="Ada"))
print(env.model_dump_json())

Wiring the Envelope into FastAPI

Declare the response model as Envelope[User] so FastAPI documents the exact nested shape in OpenAPI and validates your output.

Notice the route handler never returns bare data — it always wraps the result. That single discipline is what makes the contract stable.

from fastapi import FastAPI

app = FastAPI()

@app.get("/users/{user_id}", response_model=Envelope[User])
async def read_user(user_id: int):
    user = User(id=user_id, name="Ada")
    return Envelope[User](data=user, meta={"request_id": "abc-123"})

@app.get("/users", response_model=Envelope[list[User]])
async def list_users():
    users = [User(id=1, name="Ada"), User(id=2, name="Linus")]
    return Envelope[list[User]](data=users, meta={"count": len(users)})

Designing the Meta Block

The meta block holds information about the response that is not the resource itself. Keep it predictable so clients can rely on key names.

Good things to standardize in meta:

  • request_id for tracing and support tickets
  • pagination for list endpoints
  • deprecation warnings without breaking the payload

Model it explicitly instead of using a loose dict so the keys are documented and stable.

from pydantic import BaseModel
from typing import Optional

class PaginationMeta(BaseModel):
    page: int
    page_size: int
    total_items: int
    total_pages: int

class Meta(BaseModel):
    request_id: Optional[str] = None
    pagination: Optional[PaginationMeta] = None

m = Meta(request_id="r-1", pagination=PaginationMeta(
    page=1, page_size=20, total_items=57, total_pages=3))
print(m.model_dump_json(indent=2))

Pagination Inside the Envelope

For list endpoints, put the page of records under data and the counts under meta.pagination. The data shape (an array) never changes, even when you add new pagination fields later.

Compute total_pages with a ceiling division so the last partial page is counted correctly.

import math

def paginate(items, page, page_size, total_items):
    total_pages = math.ceil(total_items / page_size) if page_size else 0
    return {
        "data": items,
        "meta": {
            "pagination": {
                "page": page,
                "page_size": page_size,
                "total_items": total_items,
                "total_pages": total_pages,
            }
        },
        "errors": [],
    }

result = paginate([{"id": 1}], page=3, page_size=20, total_items=57)
print(result["meta"]["pagination"])

Adding HATEOAS-Style Links

Hypermedia links let clients navigate without hard-coding URL templates. For pagination, expose self, next, and prev links. When a link does not apply (page 1 has no prev), return null rather than omitting the key — predictable presence beats conditional absence.

def build_page_links(base_url, page, total_pages, page_size):
    def url(p):
        return f"{base_url}?page={p}&page_size={page_size}"
    return {
        "self": url(page),
        "next": url(page + 1) if page < total_pages else None,
        "prev": url(page - 1) if page > 1 else None,
        "first": url(1),
        "last": url(total_pages),
    }

links = build_page_links("/users", page=1, total_pages=3, page_size=20)
for k, v in links.items():
    print(k, "->", v)

A Standard Error Shape

Errors deserve the same discipline as success. Standardize each error object so clients can branch on a stable machine-readable code instead of fragile string matching on messages.

Recommended fields per error:

  • code — stable identifier like USER_NOT_FOUND
  • message — human-readable, may change freely
  • field — which input caused it, for validation errors
from pydantic import BaseModel
from typing import Optional

class ApiError(BaseModel):
    code: str
    message: str
    field: Optional[str] = None

errors = [
    ApiError(code="VALIDATION_ERROR", message="must be a valid email", field="email"),
    ApiError(code="VALIDATION_ERROR", message="required", field="name"),
]
for e in errors:
    print(e.model_dump())

Centralizing Error Envelopes

If each route builds its own error dict, the shapes drift apart. Centralize error formatting in an exception handler so every failure exits through the same envelope.

Here a custom exception carries a stable code; a single handler wraps it into the envelope. This guarantees consumers see one error shape across the whole API.

from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse

app = FastAPI()

class DomainError(Exception):
    def __init__(self, code: str, message: str, status: int = 400):
        self.code = code
        self.message = message
        self.status = status

@app.exception_handler(DomainError)
async def handle_domain_error(request: Request, exc: DomainError):
    return JSONResponse(
        status_code=exc.status,
        content={"data": None, "meta": {}, "errors": [
            {"code": exc.code, "message": exc.message}
        ]},
    )

@app.get("/users/{user_id}")
async def read_user(user_id: int):
    raise DomainError("USER_NOT_FOUND", "No user with that id", status=404)

Backward-Compatible Evolution

A stable envelope is only valuable if it can grow without breaking clients. Follow additive-change rules:

  • Safe: add new optional fields to meta or data
  • Safe: add a new error code value
  • Breaking: rename or remove an existing field
  • Breaking: change a field's type (string to object)

When you must break the contract, do it behind a new API version (for example /v2), never silently inside /v1.

Tying It Together

A complete list endpoint combines all four pillars: data for the page, meta for pagination, links for navigation, and errors as an always-present array. Because the skeleton never changes, clients written today keep working tomorrow.

This pure-Python builder shows the final assembled shape an endpoint would serialize.

import math

def build_envelope(items, page, page_size, total_items, base_url):
    total_pages = math.ceil(total_items / page_size) if page_size else 0
    def url(p):
        return f"{base_url}?page={p}&page_size={page_size}"
    return {
        "data": items,
        "meta": {
            "pagination": {
                "page": page, "page_size": page_size,
                "total_items": total_items, "total_pages": total_pages,
            }
        },
        "links": {
            "self": url(page),
            "next": url(page + 1) if page < total_pages else None,
            "prev": url(page - 1) if page > 1 else None,
        },
        "errors": [],
    }

env = build_envelope([{"id": 1}, {"id": 2}], 1, 20, 42, "/users")
print(env["meta"]["pagination"]["total_pages"], env["links"]["next"])

Quick Check: Backward Compatibility

Your /v1/users endpoint returns an envelope with data, meta, and errors. You now need to expose a new last_login timestamp on each user, and existing mobile clients must keep working unchanged.

Recap: Stable Response Envelopes

You designed a predictable, backward-compatible response contract for FastAPI:

  • One envelope wrapping every response in data, meta, and errors
  • Generic Pydantic models so a single envelope works for any payload while OpenAPI still documents the inner shape
  • Pagination under meta.pagination with ceiling-division page counts
  • Hypermedia links that return null rather than disappearing
  • Standard errors with stable machine-readable code values, centralized in one exception handler
  • Additive evolution: add optional fields freely, but put breaking changes behind a new version

Consumers can now write one parser that survives years of API growth.

자주 묻는 질문

“안정적인 응답 봉투 설계” 강의는 무료인가요?

네 — “안정적인 응답 봉투 설계” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 FastAPI Backend Development Bootcamp 강의 전체를 잠금 해제할 수 있습니다. FastAPI Backend Development Bootcamp 강의에는 총 4개의 강의가 포함되어 있습니다.

“안정적인 응답 봉투 설계”에서 뭘 배우나요?

메타데이터, 링크 및 오류 형식을 표준화해 API 소비자가 예측 가능하고 하위 호환되는 데이터를 받도록 합니다. 브라우저에서 직접 실행하는 실습 코드로 FastAPI Backend Development Bootcamp을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.

FastAPI Backend Development Bootcamp을(를) 시작하는 데 경험이 필요한가요?

사전 경험은 필요하지 않습니다. CoddyKit의 FastAPI Backend Development Bootcamp은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 4번째 강의입니다.

“안정적인 응답 봉투 설계” 강의는 얼마나 걸리나요?

대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.

이 FastAPI Backend Development Bootcamp 강의에서 코드를 작성하고 실행할 수 있나요?

네. 모든 FastAPI Backend Development Bootcamp 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.

이 강의의 모든 강의

  1. URL, 헤더 및 미디어 유형 버전 관리
  2. 대규모 환경에서의 커서와 오프셋 페이지 매김
  3. 동적 필터링 및 정렬 매개변수
  4. 안정적인 응답 봉투 설계
← FastAPI Backend Development Bootcamp(으)로 돌아가기