Практика 4. Практическая работа 4. CRUD-ресурс на FastAPI
Раздел 2. Разработка интернет-приложений (backend, Python/FastAPI)
Цель
- Закрепить материал лекции 5 (REST, CRUD, коды состояния) на практике.
- Реализовать полноценный CRUD-ресурс «Задачи» на FastAPI с хранением данных в памяти (без БД).
- Покрыть все операции CRUD:
GET(список и по id),POST,PUT,PATCH,DELETE. - Научиться возвращать правильные коды состояния:
200,201,204,404. - Разобраться в разнице между полной заменой (
PUT) и частичным обновлением (PATCH).
Теория (кратко)
CRUD — четыре базовые операции над ресурсом: Create, Read, Update, Delete. В REST они сопоставляются с HTTP-методами:
| Метод | Операция | Над чем | Код успеха |
|---|---|---|---|
GET /tasks | Read (список) | коллекция | 200 OK |
GET /tasks/{id} | Read (один) | ресурс | 200 OK |
POST /tasks | Create | коллекция | 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-taskscd fastapi-taskspython -m venv .venv.\.venv\Scripts\Activate.ps1python -m pip install --upgrade pippip 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, statusfrom 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/999 — 404.
Шаг 3. Создание ресурса (POST → 201)
# 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. Удаление (DELETE → 204)
# 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/tasksInvoke-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.
Дополнительные задания (на повышенную оценку)
- Добавьте в
GET /tasksфильтр по статусу: query-параметрdone: bool | None = None. - Добавьте проверку дубликата при создании: если задача с таким
titleуже есть —409 Conflict. - Добавьте эндпойнт
GET /tasks/statsс подсчётомtotal,done_count,pending_count.
Критерии оценки
| Критерий | Доля |
|---|---|
Окружение настроено, сервер запускается, /docs открывается | 10% |
GET /tasks и GET /tasks/{id} работают, при отсутствии — 404 | 20% |
POST /tasks создаёт ресурс и возвращает 201 | 20% |
PUT (полная замена) и PATCH (частичное обновление) реализованы корректно | 25% |
DELETE возвращает 204, повторное удаление — 404 | 15% |
Корректные коды состояния, понятные detail в ошибках | 10% |
| Итого | 100% |
Дополнительные задания дают до +15% сверх базовой оценки (но не выше 100%).
Вопросы для самопроверки
- Что такое CRUD и как операции CRUD сопоставляются с HTTP-методами?
- Чем отличается
PUTотPATCH? Когда какой метод уместен? - Почему при отсутствии ресурса нужно вернуть
404, а не200с текстом ошибки? - Какой код состояния возвращают при успешном создании ресурса? При удалении?
- Зачем для
PATCHзаводят отдельную модель с необязательными полями? - Что делает
model_dump(exclude_unset=True)и почему это важно дляPATCH? - Какие из методов CRUD идемпотентны, а какие — нет?
- Как в FastAPI задать код состояния ответа и как вернуть ошибку с нужным кодом?
- Почему хранение в словаре «теряет» данные при перезапуске сервера и как это решают в реальных проектах?