python-fastapi
Skill2 starsChanged 37 days agoMITView on GitHub
Opinionated rules for production Python APIs built with FastAPI, Pydantic v2, SQLAlchemy 2.0 (async), Alembic, and PostgreSQL, including JWT/OAuth authentication, authorization, error handling, pagination, background jobs, and testing. Use when creating or reviewing FastAPI endpoints, request/response schemas, services, database access, dependencies, auth, settings, workers, or API tests, or when debugging slow or failing API requests.
2 other projects use a version of this.
Adopted by
Most recently taken up by
- nek1987/auto-agent-harnesstheir own version, first seen Oct 11, 2026
- KaribuLab/skillstheir own version, first seen Oct 9, 2026
Other versions2 projects use a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 2
- nek1987/auto-agent-harness★ 4same name
- KaribuLab/skills★ 1same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository5 other files it gives its agents.
- expo-react-nativeSkill
- fullstack-architectSkill
- nextjs-frontendSkill
- postgresqlSkill
- swift-iosSkill
The file
We split this skill into the rules it gives an agent
11 rules. About 0 are common, 2 are distinctive, and 9 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈2
Distinctive · estimate
This author's own choices. Worth a look.
9
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 20
Use postgresql for schema design, indexes, query plans, and migration safety.
- Very specific: line 30
One error format: Every error, including validation errors and unhandled exceptions, returns the same Problem Details body with a stable …
- Very specific: line 59
camelCase on the wire: All schemas inherit one APISchema base with alias_generator=to_camel and populate_by_name=True.
- Very specific: line 111
PATCH: use data.model_dump(exclude_unset=True) to tell "not sent" apart from "set to null".
- Very specific: line 116
Upserts and races: use insert(...).on_conflict_do_update/do_nothing (PostgreSQL dialect) instead of select-then-insert.
- Very specific: line 117
Outbound HTTP: use the shared httpx.AsyncClient, explicit timeouts, and retries only for idempotent calls.
- Very specific: line 118
Background work: use FastAPI BackgroundTasks only for best-effort work you can afford to lose.
- Very specific: line 142
422 you didn't expect: read errors[].field. Common causes are a camelCase/snake_case alias mismatch, a scalar parameter that FastAPI treats …
- Distinctive: line 155
Add Redis caching only after measurement shows a hot, expensive, read-mostly query, with keys that include every scoping dimension (user, …
- Distinctive: line 166
Webhooks in: verify signatures, and deduplicate on the provider's event ID.
- Very specific: line 171
Use pytest with the anyio plugin (or pytest-asyncio) and httpx.AsyncClient(transport=ASGITransport(app=app)).
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Python FastAPI
Build the API that owns business logic, authorization, and the database for every client: Next.js web, Expo mobile, and native iOS. Stack: FastAPI, Pydantic v2, SQLAlchemy 2.0 with AsyncSession + asyncpg, Alembic, and PostgreSQL. Tooling: uv, ruff, and a type checker (mypy or pyright) in strict mode on app/.
When to use
- Adding or changing endpoints, schemas, services, models, dependencies, middleware, or settings.
- Implementing authentication, authorization, pagination, uploads, or background work.
- Writing API tests. Debugging 4xx/5xx responses, slow endpoints, or connection pool problems.
Use postgresql for schema design, indexes, query plans, and migration safety. Use fullstack-architect for API contract and auth decisions that affect clients.
Core rules
- Three layers, one direction. The router handles HTTP: parsing, auth dependencies, status codes. The service holds business rules and the transaction. Queries use SQLAlchemy. Routers contain no queries. Services never import
Requestand never raiseHTTPException. They raise domain errors, which a handler maps to HTTP. - Explicit schemas per operation. Separate
OrderCreate,OrderUpdate, andOrderRead(orOrderSummary). Never return ORM objects without a response schema, and never accept ORM models as input. Input schemas useextra="forbid". Fields the client must not control (owner_id,role,status,is_verified) don't appear in input schemas at all. - One commit per unit of work, in the service. Do all writes, then commit once with
await session.commit(). Never commit in repositories, and never commit in a dependency's teardown: if that commit fails after the response is sent, the client sees success for a write that didn't happen. - Async all the way, or not at all. In an
async defendpoint every I/O call must be awaited (asyncpg,httpx.AsyncClient). A blocking call such asrequests,boto3network calls,time.sleep, or heavy CPU work stalls every request on the worker. Move it toanyio.to_thread.run_sync, use adefendpoint, or send it to a job. - Authorization lives in queries. Scope every read and write by the principal:
WHERE id = :id AND author_id = :user_id, or by tenant. Return 404 for resources the caller can't see. - Fail fast on configuration. Settings come from
pydantic-settingsand are validated at import. A missing secret stops the process at startup, not on first use. - One error format. Every error, including validation errors and unhandled exceptions, returns the same Problem Details body with a stable
codeand therequestId.
Architecture
api/
├── pyproject.toml # uv, ruff, mypy/pyright, pytest
├── alembic.ini
├── migrations/versions/
├── app/
│ ├── main.py # create_app(): routers, middleware, error handlers, lifespan
│ ├── core/
│ │ ├── config.py # Settings(BaseSettings)
│ │ ├── db.py # engine, async_sessionmaker, Base, get_session
│ │ ├── security.py # token verification, password hashing
│ │ ├── errors.py # DomainError hierarchy + exception handlers
│ │ ├── schemas.py # APISchema base (camelCase), Page[T]
│ │ └── pagination.py # opaque cursor encode/decode
│ ├── auth/ # dependencies.py (get_current_user, require_role), router.py, service.py
│ ├── orders/ # one package per domain
│ │ ├── models.py
│ │ ├── schemas.py
│ │ ├── service.py
│ │ └── router.py
│ └── worker/ # job handlers (same codebase, different entrypoint)
└── tests/- Package by domain (
orders/,users/), not by layer (models/,schemas/,routers/). - camelCase on the wire. All schemas inherit one
APISchemabase withalias_generator=to_camelandpopulate_by_name=True. FastAPI serializes by alias by default. TypeScript and Swift clients receive camelCase without client-side key transforms. - Protected by default. Include routers with
dependencies=[Depends(get_current_user)]. Public endpoints (sign-in, health, app config) live in a separate router so that exposing one is a deliberate choice. - Startup and shutdown use
lifespan, not@app.on_event. Create one sharedhttpx.AsyncClientwith explicit timeouts there, and dispose of the engine on shutdown. - Migrations never run on startup. Alembic runs as a separate deploy step (see the
postgresqlskill).
Reference code for each core module is in references/project-skeleton.md.
Preferred patterns
# app/orders/schemas.py
class OrderCreate(APISchema):
model_config = ConfigDict(extra="forbid") # no price, total, status, or customer fields
items: Annotated[list[OrderItemIn], Field(min_length=1, max_length=50)]
note: Annotated[str | None, StringConstraints(strip_whitespace=True, max_length=500)] = None
class OrderRead(APISchema):
id: UUID
status: OrderStatus
items: list[OrderItemRead]
total: Money # integer minor units + currency
created_at: datetime # serialized as createdAt, ISO 8601 with offset
class OrderPage(Page[OrderRead]):
"""Named subclass, so OpenAPI and generated clients see `OrderPage`, not `Page_OrderRead_`."""# app/orders/router.py
router = APIRouter(prefix="/orders", tags=["orders"])
@router.post("", status_code=status.HTTP_201_CREATED)
async def create_order(payload: OrderCreate, session: SessionDep, user: CurrentUser) -> OrderRead:
return await service.create_order(session, customer=user, data=payload)# app/orders/service.py
async def create_order(session: AsyncSession, *, customer: User, data: OrderCreate) -> OrderRead:
prices = await current_prices(session, [item.product_id for item in data.items]) # never trust client prices
order = Order(customer_id=customer.id, note=data.note, items=build_items(data.items, prices))
session.add(order)
await session.flush() # assigns order.id inside the same transaction
session.add(Job(kind="email.order_confirmation", payload={"orderId": str(order.id)})) # commits atomically with the order
await session.commit()
return await get_order(session, order_id=order.id, customer_id=customer.id)- Dependencies:
SessionDep = Annotated[AsyncSession, Depends(get_session)]andCurrentUser = Annotated[User, Depends(get_current_user)]. Dependencies load things (session, user, the resource being acted on) and enforce auth. They don't run business logic. - PATCH: use
data.model_dump(exclude_unset=True)to tell "not sent" apart from "set to null". - N+1 prevention: declare relationships with
lazy="raise", so implicit lazy loads fail loudly in development. Load explicitly withselectinloadfor collections andjoinedloadfor many-to-one. - Sessions:
async_sessionmaker(engine, expire_on_commit=False), so reading attributes after commit doesn't trigger implicit I/O (MissingGreenlet). - Pagination: cursor (keyset) by default. Use
limit: Annotated[int, Query(ge=1, le=100)] = 20and return{items, nextCursor}with an opaque cursor encoding the sort key. Fetchlimit + 1rows to computenextCursorwithout aCOUNT(*). - Constraint violations: catch
IntegrityError, inspect the constraint name (uq_users_email), and raiseConflict(code="email_taken"). The database enforces the rule, and the service translates it. - Upserts and races: use
insert(...).on_conflict_do_update/do_nothing(PostgreSQL dialect) instead of select-then-insert. - Outbound HTTP: use the shared
httpx.AsyncClient, explicit timeouts, and retries only for idempotent calls. Never keep a database transaction open across an external call. - Background work: use FastAPI
BackgroundTasksonly for best-effort work you can afford to lose. Push notifications, emails, media processing, and webhooks go to a durable job queue, enqueued in the same transaction as the business write (outbox or a Postgres-backed queue). Jobs are idempotent. - Auth: verify JWTs with PyJWT. Pin
algorithms, verifyexp,iss, andaud, and requiresub. Hash passwords with Argon2 viapwdlib[argon2]. Load the user bysubon every request (a primary key lookup), so bans and role changes apply immediately. Token lifecycle: see thefullstack-architectskill.
Patterns to avoid
| Avoid | Why / instead |
Returning ORM objects or dicts without a response schema | Leaks columns and couples the API to the schema. Use explicit *Read schemas |
One PostSchema with every field optional, used for create, update, and read | Ambiguous contract. Use a schema per operation |
session.commit() in repositories, or several commits per request | Partial writes on failure. Commit once in the service |
Commit in a yield dependency's teardown | Commit failures surface after the response is sent |
requests, a sync DB driver, or time.sleep inside async def | Blocks the event loop for every request |
HTTPException raised from services | Couples domain logic to HTTP. Raise domain errors |
| Offset pagination on growing tables | Slow at depth and unstable under inserts. Use cursors |
Base.metadata.create_all() outside tests | Bypasses migrations. Use Alembic |
alembic upgrade in lifespan or the container entrypoint | Races between replicas and crash loops. Use a release step |
Bare except Exception: pass | Hides failures. Catch specific exceptions, or log and re-raise |
python-jose and passlib in new code | Poorly maintained. Use PyJWT and pwdlib |
allow_origins=["*"] with allow_credentials=True | Lets any site make credentialed calls. Use an explicit allowlist |
| Logging request bodies, tokens, or passwords | Credential and PII leaks. Log IDs and request IDs |
Debugging
MissingGreenlet/ "greenlet_spawn has not been called": an implicit lazy load or expired attribute was touched in async code. Addselectinload/joinedload, keepexpire_on_commit=False, orawait session.refresh(obj, ["relationship"]).- 422 you didn't expect: read
errors[].field. Common causes are a camelCase/snake_case alias mismatch, a scalar parameter that FastAPI treats as a query parameter when you meant the body, orextra="forbid"rejecting a field the client sends. - Slow endpoint: count queries per request (SQLAlchemy event listener or
echo=Truelocally) to find N+1, thenEXPLAIN (ANALYZE, BUFFERS)the slowest query (see thepostgresqlskill). - Latency spikes across unrelated endpoints under load: something is blocking the event loop. Run with
PYTHONASYNCIODEBUG=1to log slow callbacks, and grep for sync clients inasync def. QueuePool limit ... reached/ connection timeouts: sessions are leaking, or transactions are held open across slow awaits (external HTTP, file uploads). Checkpg_stat_activityforidle in transaction.- "prepared statement does not exist" behind PgBouncer: transaction pooling conflicts with asyncpg's statement cache. Configure the cache as described in the SQLAlchemy asyncpg docs, or enable PgBouncer ≥ 1.21
max_prepared_statements. - Include the request ID in every log line, so you can trace one request from client to database.
Performance
- Pool math: (
pool_size+max_overflow) × processes × replicas must stay below Postgresmax_connectionsminus headroom. Add PgBouncer before raisingmax_connections. - In containers, run one Uvicorn process per container and scale replicas. Use
--workerson bare VMs. - List endpoints select only the columns they serialize. Cap page size. Never return unbounded collections.
- Set
statement_timeoutandidle_in_transaction_session_timeouton the application database role, so a runaway query can't hold a connection indefinitely. - Add Redis caching only after measurement shows a hot, expensive, read-mostly query, with keys that include every scoping dimension (user, tenant).
Security
- Object-level authorization on every resource endpoint, with a test that requests another user's resource and gets 404 or 403.
- Mass assignment:
extra="forbid"on input. Ownership and roles come from the principal, never from the body. - JWT: pin algorithms (never accept
noneor follow the token'salgblindly) and verifyaud/iss. Use RS256/ES256 with JWKS when more than one service verifies tokens. - Secrets: compare tokens with
secrets.compare_digest. Store refresh tokens hashed (SHA-256 is fine for high-entropy tokens) and passwords with Argon2. - SQL: bound parameters only.
text()gets:params. Never build SQL with f-strings. - Rate limits on sign-in, token refresh, password reset, OTP, uploads, and content creation, at the edge or with a shared store.
- Docs exposure: disable
docs_url/openapi_urlin production for private APIs. Generateopenapi.jsonin CI fromapp.openapi()instead. - Webhooks in: verify signatures, and deduplicate on the provider's event ID.
- Audit dependencies with
pip-auditin CI.
Testing
- Use pytest with the anyio plugin (or pytest-asyncio) and
httpx.AsyncClient(transport=ASGITransport(app=app)). - Use real PostgreSQL with the same major version and extensions (PostGIS image) via docker compose or testcontainers. Never SQLite, which differs in types, constraints, locking, and has no PostGIS.
- Build the schema by running Alembic migrations once per test session, which tests the migrations too. Run each test inside a transaction that is rolled back (
join_transaction_mode="create_savepoint"). - Override auth with
app.dependency_overrides[get_current_user]. - Cover per endpoint: the happy path, validation failure (422 with the field), unauthenticated (401), another user's resource (404/403), constraint conflict (409), and pagination boundaries (empty page, last page, invalid cursor).
- In CI:
alembic upgrade headon an empty database,alembic check(no model/migration drift),ruff check,ruff format --check, and the type checker.
Production-readiness checklist
- Every endpoint has explicit request and response schemas. The generated OpenAPI is reviewed.
- Routers are protected by default. Object-level authorization is tested for every resource.
- No blocking I/O in
async def. All outbound HTTP has timeouts. - One commit per unit of work in the service, with no commit in dependency teardown.
- Relationships use
lazy="raise". List endpoints eager-load what they serialize. - Growing lists use cursor pagination with a capped limit.
- One Problem Details error format, including 422 and 500. No stack traces in responses.
- Structured JSON logs with
requestId. No tokens, passwords, or unnecessary PII. /health/live(process up) and/health/ready(database reachable). Neither runs migrations.- Settings are validated at startup. Secrets come from the environment or a secret manager.
- Migrations run as a separate release step.
alembic checkis clean. - Durable jobs for must-happen side effects, with retries, idempotency, and dead-letter alerting.
- Rate limits on auth and write-heavy endpoints.
- Graceful shutdown: lifespan closes HTTP clients and disposes of the engine.
- Tests run against PostgreSQL + PostGIS in CI.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

