API — practice-recipes router¶
backend/src/routers/practice_recipes.py (394 lines).
APIRouter(prefix="/practice-recipes", tags=["practice-recipes"])
(practice_recipes.py:68). Library + apply-to-user-practice endpoints
(practice_recipes.py:1-21).
| Method | Path | Auth | Request DTO | Response | Success | Errors |
|---|---|---|---|---|---|---|
| GET | /practice-recipes/?mode= |
JWT | PaginationParams, optional mode filter |
Page[PracticeRecipeOut] or bare list |
200 | — |
| POST | /practice-recipes/ |
JWT | PracticeRecipeCreate |
PracticeRecipeOut |
201 | 409 recipe_slug_taken, 400 recipe_persist_failed |
| GET | /practice-recipes/{recipe_id} |
JWT | — | PracticeRecipeOut |
200 | 404 practice_recipe_not_found |
| PATCH | /practice-recipes/{recipe_id} |
JWT | PracticeRecipeUpdate |
PracticeRecipeOut |
200 | 404; 403 cannot_modify_system_recipe; 400 recipe_invariant_violated |
| DELETE | /practice-recipes/{recipe_id} |
JWT | — | — | 204 | 404; 403 cannot_modify_system_recipe |
| POST | /practice-recipes/{recipe_id}/apply-to/{user_practice_id} |
JWT + owned user-practice | — | UserPracticeDetail |
200 | 404 practice_recipe_not_found / user_practice_not_found / practice_not_found; 403; 422 (malformed config); 400 mode_mismatch |
Notes:
- Visibility / mutation mirror practice-tags: system recipes
(
owner_user_id IS NULL) are readable but immutable — mutation is 403cannot_modify_system_recipe; the client's "edit a system recipe" flow forks a personal copy via POST with a new slug (practice_recipes.py:7-9,20-21). - Listing is N+1-free (issue #470): steps are batched into one
WHERE recipe_id IN (...)query, and pagination slices recipe rows before hydration "so the step lookup stays bounded to the current page" (practice_recipes.py:198-206). - PATCH replaces wholesale: name + description + rounds + steps;
slug and mode are immutable ("a slug rename would break any
UserPractice that captured the recipe by id, and a mode swap would
invalidate the step tag mappings"). Concurrency is deliberately
last-write-wins — recipes are personal, so "the conflict surface is
… vanishingly small" (
practice_recipes.py:270-284). - Delete cascades step rows only; UserPractice rows are unaffected —
"the recipe only ever populated their override JSON, never pointed at
the recipe by FK" (
practice_recipes.py:325-330). - Apply materialises the recipe into
UserPractice.mode_config_overridethrough the same ownership dependency and_validate_mode_config_against_cataloggate as the customise endpoint, "so the override invariant ('mode may not change') cannot be bypassed via the recipe path" — identical error shapes: 422 structured field errors, 400mode_mismatch(practice_recipes.py:14-18,349-358).
DTOs: backend/src/schemas/practice_recipe.py. Model + constraints:
data-model/practice; resolution rule:
domain/practice-resolution.
Grounded in adepthood@fbc529d, 2026-07-31.