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

Практика 4. Практическая работа 4. CRUD-ресурс на FastAPI

Раздел 2. Разработка интернет-приложений (backend, Python/FastAPI)


Цель

  1. Закрепить материал лекции 5 (REST, CRUD, коды состояния) на практике.
  2. Реализовать полноценный CRUD-ресурс «Задачи» на FastAPI с хранением данных в памяти (без БД).
  3. Покрыть все операции CRUD: GET (список и по id), POST, PUT, PATCH, DELETE.
  4. Научиться возвращать правильные коды состояния: 200, 201, 204, 404.
  5. Разобраться в разнице между полной заменой (PUT) и частичным обновлением (PATCH).

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

CRUD — четыре базовые операции над ресурсом: Create, Read, Update, Delete. В REST они сопоставляются с HTTP-методами:

МетодОперацияНад чемКод успеха
GET /tasksRead (список)коллекция200 OK
GET /tasks/{id}Read (один)ресурс200 OK
POST /tasksCreateколлекция201 Created
PUT /tasks/{id}Update (полная замена)ресурс200 OK
PATCH /tasks/{id}Update (частично)ресурс200 OK
DELETE /tasks/{id}Deleteресурс204 No Content

Ключевые моменты:

  • PUT заменяет ресурс целиком — клиент обязан прислать все поля. Идемпотентен.
  • PATCH меняет только переданные поля — остальные сохраняются. Для этого используют схему с необязательными полями и model_dump(exclude_unset=True).
  • Если ресурс не найден — нужно вернуть 404 Not Found через HTTPException, а не 200 с текстом ошибки.
  • DELETE при успехе возвращает 204 No Content — тело ответа пустое.
  • Код состояния задаётся параметром status_code в декораторе или в HTTPException. Ошибки валидации Pydantic FastAPI отдаёт автоматически как 422.

Задание

Шаг 0. Подготовка окружения

Создайте проект и установите зависимости (Windows, PowerShell):

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

Создайте файл main.py. Сервер запускается командой uvicorn main:app --reload, Swagger UI — по адресу http://127.0.0.1:8000/docs.


Шаг 1. Модели данных и «база» в памяти

Опишем ресурс «Задача» через Pydantic-модели и заведём словарь как хранилище.

from fastapi import FastAPI, HTTPException, status
from pydantic import BaseModel, Field
app = FastAPI(title="Tasks API (Practice 4)", version="1.0.0")
# ---------- Pydantic-модели ----------
class TaskCreate(BaseModel):
title: str = Field(..., min_length=1, description="Название задачи")
description: str = Field(default="", description="Описание")
done: bool = Field(default=False, description="Выполнена?")
class Task(TaskCreate):
id: int
class TaskUpdate(BaseModel): # для PATCH: все поля необязательны
title: str | None = None
description: str | None = None
done: bool | None = None
# ---------- Хранилище в памяти ----------
tasks: dict[int, Task] = {}
counter = 0

TaskCreate используется для POST и PUT (нужны все поля), а TaskUpdate — для PATCH (поля необязательны).


Шаг 2. Чтение: список и один ресурс (GET)

# READ — список всех задач
@app.get("/tasks", response_model=list[Task])
def list_tasks():
return list(tasks.values())
# READ — одна задача по id
@app.get("/tasks/{task_id}", response_model=Task)
def get_task(task_id: int):
task = tasks.get(task_id)
if task is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,
detail="Task not found")
return task

Проверьте: GET /tasks на пустом хранилище вернёт [] с кодом 200, а GET /tasks/999404.


Шаг 3. Создание ресурса (POST201)

# CREATE
@app.post("/tasks", response_model=Task,
status_code=status.HTTP_201_CREATED)
def create_task(data: TaskCreate):
global counter
counter += 1
task = Task(id=counter, **data.model_dump())
tasks[counter] = task
return task

Тело запроса { "title": "Сдать лабу", "done": false } → ответ с кодом 201 Created и присвоенным id.


Шаг 4. Полная замена (PUT) и частичное обновление (PATCH)

# UPDATE — полная замена (нужны ВСЕ поля)
@app.put("/tasks/{task_id}", response_model=Task)
def replace_task(task_id: int, data: TaskCreate):
if task_id not in tasks:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,
detail="Task not found")
task = Task(id=task_id, **data.model_dump())
tasks[task_id] = task
return task
# UPDATE — частичное обновление (только переданные поля)
@app.patch("/tasks/{task_id}", response_model=Task)
def update_task(task_id: int, data: TaskUpdate):
task = tasks.get(task_id)
if task is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,
detail="Task not found")
updated = task.model_copy(update=data.model_dump(exclude_unset=True))
tasks[task_id] = updated
return updated

Сравните поведение:

  • PUT /tasks/1 — семантика полной замены: клиент присылает все поля, ресурс перезаписывается целиком.
  • PATCH /tasks/1 с телом { "done": true } → изменится только done, остальные поля сохранятся (за это отвечает exclude_unset=True).

Шаг 5. Удаление (DELETE204)

# DELETE
@app.delete("/tasks/{task_id}",
status_code=status.HTTP_204_NO_CONTENT)
def delete_task(task_id: int):
if tasks.pop(task_id, None) is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,
detail="Task not found")
return None

При успехе сервер вернёт 204 No Content без тела. Повторный DELETE того же id даст 404.


Шаг 6. Проверка через PowerShell

Окно терминала
# Создать
Invoke-RestMethod -Method Post -Uri http://127.0.0.1:8000/tasks `
-ContentType "application/json" -Body '{"title":"Купить хлеб","done":false}'
# Список / частично обновить (id=1) / удалить
Invoke-RestMethod -Uri http://127.0.0.1:8000/tasks
Invoke-RestMethod -Method Patch -Uri http://127.0.0.1:8000/tasks/1 `
-ContentType "application/json" -Body '{"done":true}'
Invoke-RestMethod -Method Delete -Uri http://127.0.0.1:8000/tasks/1

Удобнее всё то же самое тестировать через Swagger UI на /docs.


Дополнительные задания (на повышенную оценку)

  1. Добавьте в GET /tasks фильтр по статусу: query-параметр done: bool | None = None.
  2. Добавьте проверку дубликата при создании: если задача с таким title уже есть — 409 Conflict.
  3. Добавьте эндпойнт GET /tasks/stats с подсчётом total, done_count, pending_count.

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

КритерийДоля
Окружение настроено, сервер запускается, /docs открывается10%
GET /tasks и GET /tasks/{id} работают, при отсутствии — 40420%
POST /tasks создаёт ресурс и возвращает 20120%
PUT (полная замена) и PATCH (частичное обновление) реализованы корректно25%
DELETE возвращает 204, повторное удаление — 40415%
Корректные коды состояния, понятные detail в ошибках10%
Итого100%

Дополнительные задания дают до +15% сверх базовой оценки (но не выше 100%).


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

  1. Что такое CRUD и как операции CRUD сопоставляются с HTTP-методами?
  2. Чем отличается PUT от PATCH? Когда какой метод уместен?
  3. Почему при отсутствии ресурса нужно вернуть 404, а не 200 с текстом ошибки?
  4. Какой код состояния возвращают при успешном создании ресурса? При удалении?
  5. Зачем для PATCH заводят отдельную модель с необязательными полями?
  6. Что делает model_dump(exclude_unset=True) и почему это важно для PATCH?
  7. Какие из методов CRUD идемпотентны, а какие — нет?
  8. Как в FastAPI задать код состояния ответа и как вернуть ошибку с нужным кодом?
  9. Почему хранение в словаре «теряет» данные при перезапуске сервера и как это решают в реальных проектах?