Практика 2. Практическая работа 2. HTTP-запросы и первые REST-эндпоинты
Цель
- Закрепить теорию HTTP (методы, заголовки, коды состояния) на практике.
- Научиться писать GET-эндпоинты с path-параметрами (
/books/{id}) и query-параметрами (?author=...&limit=...). - Научиться читать HTTP-заголовки запроса (
User-Agent,Accept-Language, свойX-Request-ID). - Реализовать разные методы для одного ресурса и осознанно выбирать коды ответа (
200,201,204,400,404). - Проверять эндпоинты тремя способами: через браузер, 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-ID→x_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-2cd fastapi-books-2python -m venv .venv.\.venv\Scripts\Activate.ps1python -m pip install --upgrade pippip install fastapi uvicornСоздайте файл main.py с каркасом и тестовыми данными:
from fastapi import FastAPI, HTTPException, Path, Query, Header, statusfrom 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/whoamicurl -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 и тело с новым idcurl -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: ждём 404curl -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Проверка:
# Бизнес-ошибка → 400curl -i -X POST http://127.0.0.1:8000/books/validated \ -H "Content-Type: application/json" \ -d '{"title": "Из 3000 года", "author": "Аноним", "year": 3000}'
# Ошибка типа (year строкой) → 422 от Pydanticcurl -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-параметром, корректный 404 | 15% |
| Шаг 2: GET с query-параметрами, фильтры и пагинация работают | 20% |
Шаг 3: чтение заголовков (User-Agent, X-Request-ID) | 15% |
Шаг 4: POST (201) и DELETE (204) на одном URI | 20% |
Шаг 5: осознанный выбор 400 против 422 | 15% |
| Проверка через curl/Swagger и краткие пояснения к кодам ответа | 15% |
| Итого | 100% |
Дополнительно: выполненный шаг 6 (сортировка) — +10% сверх базовых 100%.
Вопросы для самопроверки
- Чем path-параметр отличается от query-параметра? Приведите пример каждого.
- Почему фильтрацию делают через query-строку, а не через путь?
- Как в FastAPI прочитать заголовок
X-Request-ID? Как имя заголовка превращается в имя аргумента функции? - Какой код вернуть при успешном создании ресурса? А при удалении без тела ответа?
- В чём разница между
400и422? Какой из них FastAPI отдаёт автоматически? - Почему возвращать
200 OKс телом{"error": ...}— плохая практика? - Чем
ge=1вPath(...)отличается от ручной проверкиif book_id < 1? Какой код ответа получится в каждом случае? - Сколько разных HTTP-методов может обслуживать один и тот же URI? Приведите пример.