Verified against GitHub Copilot Chat · 2026-07-21
Scaffold a FastAPI endpoint with real validation and status codes
A prompt that turns an endpoint requirement into a complete FastAPI route with Pydantic v2 models, correct HTTP status codes, and dependency-injected services, instead of a bare route stub returning a dict.
The prompt
Ready to copy — highlighted parts are example details you can swap.
Scaffold one FastAPI endpoint. This is a real service route, not a toy example — include validation, correct status codes, and dependency injection, the way it would actually ship. ENDPOINT REQUIREMENT POST a new subscription for a user: takes plan_id and payment_method_id, returns the created subscription, 404 if plan_id doesn't exist, 409 if the user already has an active subscription. CONVENTIONS TO FOLLOW - Use APIRouter, mounted under /api/v1, resource name subscriptions. - Request and response bodies are Pydantic v2 models (BaseModel, not raw dicts) with Field constraints matching the requirement — don't accept a wider type than the spec allows. - Use response_model on the route decorator, and the correct status code from fastapi.status (201 for creation, 204 for delete with no body, 404 when a lookup fails — 422 is Pydantic's job to raise automatically, not yours to hand-roll). - Inject dependencies (DB session, current user, rate limiter, etc.) via Depends(...), never instantiate them inside the handler. Assume this dependency already exists and is wired: get_db_session() -> AsyncSession, already defined in app/api/deps.py. - Auth: requires an authenticated user via get_current_user(). If it's "none," don't add an auth dependency; don't invent one either just because most routes in a typical app have one. - All I/O is async def. If a called function is genuinely synchronous or CPU-bound, say so explicitly and wrap it with run_in_threadpool rather than blocking the event loop silently inside an async handler. - Raise HTTPException with a specific status and a specific detail message for every failure path named in the requirement — don't let an unhandled case fall through to a generic 500 that tells the caller nothing. - Concurrency edge case: Two requests from the same user could both pass the "no active subscription" check before either commits, creating two active subscriptions.. If two requests could race on the same resource, say explicitly what prevents a lost update or duplicate row, rather than assuming single-request timing. - Pagination: if the endpoint returns a list, use limit/offset or cursor-based pagination matching the resource's expected size, and cap the maximum page size server-side — never trust a client-supplied limit as unbounded, since that turns one request into an accidental full-table scan. - Idempotency: if the requirement implies a client might retry the same request (anything payment-adjacent or otherwise non-idempotent), state whether an idempotency key is needed and exactly how it's checked before the write happens — silently allowing an identical retry to double-create or double-charge is a real design gap, not a rare edge case to wave off. OUTPUT FORMAT 1. The Pydantic request/response models, with every Field constraint justified against the requirement. 2. The route function, fully wired with its dependencies. 3. A one-line note per status code used, mapping it to the exact requirement or failure path it satisfies. 4. One sentence on how Two requests from the same user could both pass the "no active subscription" check before either commits, creating two active subscriptions. is actually addressed, or a stated reason it isn't a real risk here. 5. One sentence on whether this endpoint needed pagination or idempotency handling, and what was done about it if so.
Customize
Optional — swap in your own details for the highlighted parts above.
Why this works
FastAPI derives its automatic OpenAPI schema and request validation directly from the type hints on the route function and the Pydantic models it declares, so a scaffold that skips response_model or accepts a raw dict quietly loses the framework's main benefit — it still runs, but the generated docs and validation are wrong or missing, which is invisible until a client relies on the schema and gets surprised. Naming the exact status code per failure path (404 for a missing lookup, 409 for a conflict) stops the common default of returning 200 with an error message buried in the body, which breaks every client that checks the HTTP status rather than parsing the payload for a hidden error field. The explicit instruction to wrap blocking calls with run_in_threadpool addresses FastAPI's single most common production bug: an async def handler that calls a synchronous, blocking function directly, which stalls the entire event loop for every other request being served by that worker, not just the slow one — a bug that is invisible under a load test with one client and catastrophic under real concurrent traffic. The concurrency_note field earns its place because Pydantic validation and a 409 check both run per-request in isolation — nothing about validating one request's body prevents a second, near-simultaneous request from passing the same "no active subscription" check before either has committed, and a scaffold that only reasons about a single request in isolation will silently produce a route that allows exactly the duplicate-row race condition the 409 status code was supposed to prevent in the first place. The idempotency requirement matters for a related but distinct reason: HTTP clients and mobile apps retry on timeout by design, so a POST that isn't idempotent will occasionally execute twice for the exact same user action whenever a response is slow or dropped in transit, regardless of any concurrency locking already in place — locking prevents two different requests from racing each other, while idempotency prevents one request's own network retry from doing the same write a second time, and a scaffold that only solves the first problem still leaves the second one live in production.
What you get back
class SubscriptionCreate(BaseModel): plan_id: UUID payment_method_id: str = Field(min_length=1) class SubscriptionOut(BaseModel): id: UUID plan_id: UUID status: str @router.post("/subscriptions", response_model=SubscriptionOut, status_code=status.HTTP_201_CREATED) async def create_subscription( body: SubscriptionCreate, db: AsyncSession = Depends(get_db_session), current_user: User = Depends(get_current_user), ) -> SubscriptionOut: plan = await get_plan(db, body.plan_id) if plan is None: raise HTTPException(status_code=404, detail="Plan not found") async with db.begin(): if await user_has_active_subscription(db, current_user.id, for_update=True): raise HTTPException(status_code=409, detail="User already has an active subscription") subscription = await create_subscription_record(db, current_user.id, body) return subscription Concurrency: the active-subscription check and the insert now happen inside one db.begin() transaction with a row-level lock (for_update=True), so two simultaneous requests can no longer both pass the check before either commits.
Verified against
GitHub Copilot Chat 1.261 (VS Code) · 2026-07-21
Cursor 2.1 · 2026-07-22
Changelog
- 2026-07-22 — Initial publish, verified against GitHub Copilot Chat and Cursor 2.1 on FastAPI 0.115.
Need this built into your business?
If a prompt isn't enough — custom software, built and maintained for you — that's Scult's day job.
EXPLORE CUSTOM SOFTWARE
