agentleFS
ludo-technologiesludo-technologies/python-best-practices/skills/web-application/SKILL.md

web-application

Skill34 starsChanged 4 months agoMITView on GitHub

FastAPI web application best practices covering authentication and authorization, SQLAlchemy 2.0 async sessions, transactions, eager loading, and Alembic migrations, non-blocking I/O and shared HTTP clients, request/response schemas, error responses, pagination, typed settings, app factory and lifespan, request-ID logging, and endpoint testing. Use when building, reviewing, or refactoring a FastAPI backend or REST API, or code that touches SQLAlchemy, Alembic, authentication, or HTTP endpoints. Not for Django or Flask projects.

The file

We split this skill into the rules it gives an agent

3 rules. About 0 are common, 2 are distinctive, and 1 is 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.

  • 1

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    All examples share one invoice application.

    line 18
  2. Distinctive:

    Keep the event loop free so one request cannot stall the others.

    line 46
  3. Distinctive:

    Test through the real app and a real database.

    line 85

≈ “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.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Python Web Application

Best practices for FastAPI backends. Designed for AI agents and LLMs to build web applications that are secure, correct under concurrency, and testable.

The rules assume one stack: FastAPI, SQLAlchemy 2.0 (async), Alembic, Pydantic v2 with pydantic-settings, httpx, PyJWT, and pwdlib. Examples use the async stack; the same rules apply to sync def endpoints with Session. Django and Flask are out of scope. Where a rule overlaps a coding-standards or testing rule, this skill takes precedence for web application code.

All examples share one invoice application. Its models are defined in db-sqlalchemy-2-style, its Settings in config-typed-settings, and its create_app in app-factory-lifespan; other rules show only the part they discuss. Read the matching rule file in full before writing code in its area.

Categories

Security [CRITICAL]

Authenticate users safely and authorize every access to their data.

RuleDescription
sec-password-hashingHash passwords with Argon2
sec-secure-tokensGenerate tokens with secrets and compare in constant time
sec-jwt-validationValidate JWTs with a pinned algorithm and required claims
sec-object-authorizationAuthorize access to every object, not just the endpoint
sec-cors-originsList allowed CORS origins explicitly

Database [CRITICAL]

Query, scope, and evolve the database safely with SQLAlchemy 2.0 and Alembic.

RuleDescription
db-parameterized-queriesNever build SQL with string formatting
db-session-per-requestCreate the engine once and a session per request
db-transaction-boundaryCommit once per use case, not in helpers or dependency teardown
db-eager-loadingLoad relationships explicitly and make lazy loads raise
db-migrations-alembicChange the schema with Alembic migrations
db-sqlalchemy-2-styleUse SQLAlchemy 2.0 typed models and select()

Async I/O [CRITICAL]

Keep the event loop free so one request cannot stall the others.

RuleDescription
async-no-blocking-callsNever block the event loop in async def endpoints
async-shared-http-clientShare one httpx.AsyncClient with explicit timeouts

API Design [HIGH]

Shape what enters and leaves the API.

RuleDescription
api-separate-schemasSeparate request and response models from ORM models
api-error-responsesMap domain errors to HTTP responses in exception handlers
api-bounded-listsBound and paginate every list endpoint

Configuration [HIGH]

Fail at startup, not at the first request, when configuration is wrong.

RuleDescription
config-typed-settingsLoad configuration into a typed settings object at startup

App Structure [HIGH]

Compose the app explicitly so resources and logic are replaceable and testable.

RuleDescription
app-factory-lifespanBuild the app in a factory and manage resources in lifespan
app-thin-routesKeep route functions thin and inject dependencies with Annotated

Observability [MEDIUM]

Make each request traceable through the logs.

RuleDescription
obs-request-id-loggingCorrelate logs with a request ID

Testing [MEDIUM]

Test through the real app and a real database.

RuleDescription
test-dependency-overridesTest endpoints with dependency_overrides, not patching
test-db-rollbackRun tests against a real database in a rolled-back transaction

Quick Reference

App Factory and Dependencies

def create_app(settings: Settings) -> FastAPI:
    @asynccontextmanager
    async def lifespan(app: FastAPI) -> AsyncIterator[None]:
        engine = create_async_engine(
            settings.database_url.get_secret_value(), pool_pre_ping=True
        )
        try:
            async with httpx.AsyncClient(
                base_url=settings.payments_base_url,
                timeout=httpx.Timeout(10.0, connect=3.0),
            ) as payments_client:
                app.state.sessionmaker = async_sessionmaker(
                    engine, expire_on_commit=False
                )
                app.state.payments_client = payments_client
                yield
        finally:
            await engine.dispose()

    app = FastAPI(lifespan=lifespan)
    app.state.settings = settings
    add_exception_handlers(app)
    app.include_router(invoices.router)
    return app


async def get_session(request: Request) -> AsyncIterator[AsyncSession]:
    async with request.app.state.sessionmaker() as session:
        yield session  # no commit here: teardown runs after the response is sent


SessionDep = Annotated[AsyncSession, Depends(get_session)]
CurrentUser = Annotated[User, Depends(get_current_user)]

Authorized, Eager-Loaded Query

router = APIRouter(prefix="/invoices", dependencies=[Depends(get_current_user)])


@router.get("/{invoice_id}")
async def read_invoice(
    invoice_id: int, user: CurrentUser, session: SessionDep
) -> InvoiceRead:
    invoice = await session.scalar(
        select(Invoice)
        .where(Invoice.id == invoice_id, Invoice.owner_id == user.id)  # scoped by owner
        .options(selectinload(Invoice.lines))  # relationships declared lazy="raise"
    )
    if invoice is None:
        raise NotFoundError("Invoice")  # mapped to 404 by an exception handler
    return InvoiceRead.model_validate(invoice)

Passwords and Tokens

password_hash = PasswordHash.recommended()  # Argon2id via pwdlib
hashed = await asyncio.to_thread(password_hash.hash, password)

claims = jwt.decode(
    token,
    settings.jwt_secret.get_secret_value(),
    algorithms=["HS256"],
    audience=settings.jwt_audience,
    issuer=settings.jwt_issuer,
    options={"require": ["exp", "iat", "sub", "iss", "aud"]},
)

reset_token = secrets.token_urlsafe(32)  # store only its SHA-256 hash
hmac.compare_digest(expected_signature.encode(), presented_signature.encode())

Settings and CORS

class Settings(BaseSettings):
    model_config = SettingsConfigDict(env_prefix="APP_")

    database_url: SecretStr
    jwt_secret: SecretStr = Field(min_length=32)
    jwt_issuer: str
    jwt_audience: str
    access_token_ttl_minutes: int = Field(default=15, gt=0, le=60)
    cors_origins: list[str] = []
    payments_base_url: str
    payments_api_key: SecretStr


app.add_middleware(
    CORSMiddleware,
    allow_origins=settings.cors_origins,  # never ["*"] with allow_credentials=True
    allow_credentials=True,
    allow_methods=["GET", "POST", "PATCH", "DELETE"],
    allow_headers=["Authorization", "Content-Type"],
)

Testing

@pytest.fixture
async def session(engine: AsyncEngine) -> AsyncIterator[AsyncSession]:
    async with engine.connect() as conn:
        transaction = await conn.begin()
        session = AsyncSession(
            bind=conn, join_transaction_mode="create_savepoint", expire_on_commit=False
        )
        try:
            yield session
        finally:
            await session.close()
            await transaction.rollback()


@pytest.fixture
async def client(
    session: AsyncSession, settings: Settings
) -> AsyncIterator[httpx.AsyncClient]:
    app = create_app(settings)

    async def override_session() -> AsyncIterator[AsyncSession]:
        yield session

    app.dependency_overrides[get_session] = override_session
    transport = httpx.ASGITransport(app=app)
    async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
        yield client

See Also

  • coding-standards - General Python rules; validation-pydantic, design-dependency-injection, design-no-global-singleton, and error-no-silent-exceptions underlie several rules here
  • testing - pytest structure, fixtures, and mocking that endpoint tests still follow
  • tooling - ruff, mypy, and pytest configuration; ruff's S rules catch several issues covered here

Download now (ZIP)

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
What did you use it for? e.g. Reviewing pull requests in a Rails app

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.