Перейти к содержимому

Практика 2. Практическая работа 2. HTTP-запросы и первые REST-эндпоинты

Цель

  1. Закрепить теорию HTTP (методы, заголовки, коды состояния) на практике.
  2. Научиться писать GET-эндпоинты с path-параметрами (/books/{id}) и query-параметрами (?author=...&limit=...).
  3. Научиться читать HTTP-заголовки запроса (User-Agent, Accept-Language, свой X-Request-ID).
  4. Реализовать разные методы для одного ресурса и осознанно выбирать коды ответа (200, 201, 204, 400, 404).
  5. Проверять эндпоинты тремя способами: через браузер, Swagger UI (/docs) и curl.

Теория (кратко)

  • Path-параметр — часть URI, идентифицирующая конкретный ресурс: в /books/5 число 5 — это book_id. Передаётся в функцию через Path(...).
  • Query-параметр — пара ключ=значение после ?: /books?author=Толстой&limit=10. Используется для фильтрации, сортировки и пагинации, но не для идентификации ресурса. Передаётся через Query(...).
  • Заголовки (headers) — метаданные запроса (Host, User-Agent, Accept, Authorization). В FastAPI читаются через Header(...). Дефис в имени заголовка соответствует подчёркиванию в имени аргумента: X-Request-IDx_request_id.
  • Метод выражает действие над ресурсом: GET — чтение, POST — создание, PUT/PATCH — изменение, DELETE — удаление. Действие — это метод, а не глагол в URI.
  • Код состояния обязан отражать результат: 200 OK, 201 Created, 204 No Content, 400 Bad Request, 404 Not Found, 422 (автоматически при ошибке валидации Pydantic).

Подробности — в Лекции 3 («Протокол HTTP в деталях») и Лекции 5 («REST API»).


Задание

В работе развиваем учебный сервис «Книги» из практики 1. Хранилище — словарь в памяти (без БД). Используем целочисленные id ради простоты URL.

Шаг 0. Подготовка проекта

Окно терминала
mkdir fastapi-books-2
cd fastapi-books-2
python -m venv .venv
.\.venv\Scripts\Activate.ps1
python -m pip install --upgrade pip
pip install fastapi uvicorn

Создайте файл main.py с каркасом и тестовыми данными:

from fastapi import FastAPI, HTTPException, Path, Query, Header, status
from pydantic import BaseModel
app = FastAPI(title="Books API — практика 2", version="2.0.0")
class Book(BaseModel):
id: int
title: str
author: str
year: int
# Хранилище в памяти + стартовые данные
DB: dict[int, Book] = {
1: Book(id=1, title="Война и мир", author="Толстой", year=1869),
2: Book(id=2, title="Преступление и наказание", author="Достоевский", year=1866),
3: Book(id=3, title="Идиот", author="Достоевский", year=1869),
}
@app.get("/")
def root():
return {"msg": "Books API. Откройте /docs для Swagger UI."}

Запустите сервер и держите его открытым во время всей работы:

Окно терминала
uvicorn main:app --reload

Откройте http://127.0.0.1:8000/docs — все добавляемые эндпоинты будут появляться здесь автоматически.

Шаг 1. GET с path-параметром: один ресурс

Добавьте эндпоинт получения книги по идентификатору. Если книги нет — отвечаем 404, а не пустым 200.

@app.get("/books/{book_id}", response_model=Book)
def get_book(book_id: int = Path(..., ge=1, description="Идентификатор книги")):
book = DB.get(book_id)
if book is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,
detail="Книга не найдена")
return book

Проверка:

Окно терминала
curl http://127.0.0.1:8000/books/1 # 200 OK + JSON книги
curl -i http://127.0.0.1:8000/books/999 # 404 Not Found

Флаг -i показывает статус-строку и заголовки ответа. Обратите внимание: ge=1 запретит /books/0 и вернёт 422 ещё до входа в функцию.

Шаг 2. GET с query-параметрами: фильтрация и пагинация

Реализуйте список книг с необязательными фильтрами. Query-параметры объявляются как аргументы функции со значением по умолчанию.

@app.get("/books", response_model=list[Book])
def list_books(
author: str | None = Query(None, description="Фильтр по автору (подстрока)"),
year: int | None = Query(None, description="Точный год издания"),
limit: int = Query(10, ge=1, le=100, description="Сколько вернуть"),
offset: int = Query(0, ge=0, description="Сколько пропустить"),
):
items = list(DB.values())
if author:
items = [b for b in items if author.lower() in b.author.lower()]
if year is not None:
items = [b for b in items if b.year == year]
return items[offset:offset + limit]

Проверка:

Окно терминала
curl "http://127.0.0.1:8000/books"
curl "http://127.0.0.1:8000/books?author=Достоевский"
curl "http://127.0.0.1:8000/books?year=1869&limit=1"
curl "http://127.0.0.1:8000/books?limit=2&offset=1"

В браузере достаточно открыть тот же URL. В Swagger UI у эндпоинта появятся поля для каждого параметра — удобно подбирать значения.

Шаг 3. Чтение заголовков запроса

Заголовки читаются через Header(...). Сделаем эндпоинт, который возвращает сведения о клиенте и эхо-заголовок X-Request-ID.

@app.get("/whoami")
def whoami(
user_agent: str | None = Header(None),
accept_language: str | None = Header(None),
x_request_id: str | None = Header(None, description="Произвольный идентификатор запроса"),
):
return {
"user_agent": user_agent,
"accept_language": accept_language,
"x_request_id": x_request_id,
}

Проверка (передаём свои заголовки флагом -H):

Окно терминала
curl http://127.0.0.1:8000/whoami
curl -H "X-Request-ID: abc-123" -H "Accept-Language: ru-RU" \
http://127.0.0.1:8000/whoami

Имя аргумента x_request_id автоматически сопоставляется заголовку X-Request-ID (подчёркивания → дефисы, регистр не важен).

Шаг 4. Разные методы для ресурса: POST и DELETE

Один URI обслуживает несколько методов. Добавим создание (201 Created) и удаление (204 No Content).

class BookCreate(BaseModel):
title: str
author: str
year: int
@app.post("/books", response_model=Book, status_code=status.HTTP_201_CREATED)
def create_book(data: BookCreate):
new_id = max(DB.keys(), default=0) + 1
book = Book(id=new_id, **data.model_dump())
DB[new_id] = book
return book
@app.delete("/books/{book_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_book(book_id: int):
if DB.pop(book_id, None) is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,
detail="Книга не найдена")
return None

Проверка:

Окно терминала
# Создание: ждём 201 и тело с новым id
curl -i -X POST http://127.0.0.1:8000/books \
-H "Content-Type: application/json" \
-d '{"title": "Бесы", "author": "Достоевский", "year": 1872}'
# Удаление: ждём 204 без тела
curl -i -X DELETE http://127.0.0.1:8000/books/2
# Повторное удаление того же id: ждём 404
curl -i -X DELETE http://127.0.0.1:8000/books/2

Заметьте: DELETE идемпотентен по смыслу, но второй вызов честно возвращает 404, потому что ресурс уже удалён.

Шаг 5. Осознанный выбор кода ответа (валидация → 400)

Pydantic ловит ошибки типов автоматически (422). Но бизнес-правила проверяем сами и возвращаем 400. Запретим «книги из будущего».

from datetime import date
@app.post("/books/validated", response_model=Book,
status_code=status.HTTP_201_CREATED)
def create_book_validated(data: BookCreate):
if data.year > date.today().year:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST,
detail="Год издания не может быть в будущем")
new_id = max(DB.keys(), default=0) + 1
book = Book(id=new_id, **data.model_dump())
DB[new_id] = book
return book

Проверка:

Окно терминала
# Бизнес-ошибка → 400
curl -i -X POST http://127.0.0.1:8000/books/validated \
-H "Content-Type: application/json" \
-d '{"title": "Из 3000 года", "author": "Аноним", "year": 3000}'
# Ошибка типа (year строкой) → 422 от Pydantic
curl -i -X POST http://127.0.0.1:8000/books/validated \
-H "Content-Type: application/json" \
-d '{"title": "Тест", "author": "Аноним", "year": "много"}'

Сравните: одна и та же неудача — но 400 для нарушения бизнес-правила и 422 для ошибки формата данных.

Шаг 6 (по желанию). Сортировка через query-параметр

Добавьте в GET /books параметры sort_by (year/title) и order (asc/desc). При недопустимом значении sort_by верните 400. Проверьте через Swagger UI, что сортировка работает в обе стороны.


Критерии оценки

КритерийБаллы
Шаг 1: GET с path-параметром, корректный 40415%
Шаг 2: GET с query-параметрами, фильтры и пагинация работают20%
Шаг 3: чтение заголовков (User-Agent, X-Request-ID)15%
Шаг 4: POST (201) и DELETE (204) на одном URI20%
Шаг 5: осознанный выбор 400 против 42215%
Проверка через curl/Swagger и краткие пояснения к кодам ответа15%
Итого100%

Дополнительно: выполненный шаг 6 (сортировка) — +10% сверх базовых 100%.


Вопросы для самопроверки

  1. Чем path-параметр отличается от query-параметра? Приведите пример каждого.
  2. Почему фильтрацию делают через query-строку, а не через путь?
  3. Как в FastAPI прочитать заголовок X-Request-ID? Как имя заголовка превращается в имя аргумента функции?
  4. Какой код вернуть при успешном создании ресурса? А при удалении без тела ответа?
  5. В чём разница между 400 и 422? Какой из них FastAPI отдаёт автоматически?
  6. Почему возвращать 200 OK с телом {"error": ...} — плохая практика?
  7. Чем ge=1 в Path(...) отличается от ручной проверки if book_id < 1? Какой код ответа получится в каждом случае?
  8. Сколько разных HTTP-методов может обслуживать один и тот же URI? Приведите пример.