Files
Oleg Yurchik b8388fa4a2 Add database, telegram bot, and tests skills; update stack and fastapi skills
- Add python-development-database skill: metaorm patterns for tables, repositories, filters, settings, transactions, pagination, and exceptions
- Add python-development-telegram-bot skill: aiogram bot service with polling/webhook, router structure, middlewares, inline keyboards, and pagination
- Add python-development-tests skill: test organization guidelines with pytest, fixtures, and dependency injection rules
- Update python-development-stack: replace SQLModel/SQLAlchemy with metaorm; add pytest, pytest-asyncio, pytest-cov, faker
- Update python-development-fastapi: add sections for dependencies and list endpoints with ListResponse schema
- Update python-development-code-style: allow ctx as exception for typer.Context parameter
- Update python-development index with links to all three new skills

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-10-26 14:04:05 +03:00

7.3 KiB

name, description
name description
python-development-fastapi Patterns for building a FastAPI HTTP service inside the facet service tree. Use when creating REST endpoints, routers, or the api/rest service package.

Rules

  • The FastAPI service lives in a sub-package named api/rest/ inside the root package. It is a regular facet service: it has service.py, settings.py, and __init__.py.
  • Provide a custom UvicornServer that disables signal handlers so the parent facet orchestrator controls startup and shutdown. Example:
    import uvicorn
    
    class UvicornServer(uvicorn.Server):
        def install_signal_handlers(self):
            pass
    
  • The service class inherits from facet.AsyncioServiceMixin. Its constructor accepts the settings instance declared in the same package's settings.py. Keep the constructor minimal; heavy initialization happens in start().
  • The start() method builds the FastAPI application asynchronously, configures uvicorn, and adds the server coroutine to the facet task list with self.add_task(server.serve()).
  • Extract app construction into an async _build_app() method so router registration and dependency wiring can use await.
  • Register routers in _setup_app() or _build_app() using app.include_router(...). Group related endpoints under routers with a prefix.
  • The settings class inherits from pydantic.BaseModel (not BaseSettings). It declares the minimum required for the HTTP layer: host, port, and references to other subsystem settings when the REST layer wires them directly.

Minimal complete example

api/rest/settings.py:

from pydantic import BaseModel, conint


class RESTAPISettings(BaseModel):
    host: str = "127.0.0.1"
    port: conint(ge=1, le=65535) = 8000

api/rest/service.py:

import facet
import fastapi
import uvicorn

from . import handlers, users
from .settings import RESTAPISettings


class UvicornServer(uvicorn.Server):
    def install_signal_handlers(self):
        pass


class RESTAPIService(facet.AsyncioServiceMixin):
    def __init__(self, settings: RESTAPISettings):
        self._settings = settings

    async def start(self):
        app = await self._build_app()
        config = uvicorn.Config(
            app=app,
            host=self._settings.host,
            port=self._settings.port,
        )
        server = UvicornServer(config)
        self.add_task(server.serve())

    async def _build_app(self) -> fastapi.FastAPI:
        app = fastapi.FastAPI()
        await self._setup_app(app)
        return app

    async def _setup_app(self, app: fastapi.FastAPI):
        app.get("/health")(handlers.health)
        app.include_router(users.get_users, prefix="/users")

Router and handler structure

  • Endpoints must be RESTful. The URL path structure maps directly to the package structure inside api/rest/.
  • Each URL segment becomes a sub-package. For example /users/{id} lives in api/rest/users/.
  • Every handler sub-package contains at minimum:
    • router.py — defines get_router() -> fastapi.APIRouter and wires each route to a handler function.
    • handlers.py — plain async functions that implement the endpoint logic.
    • schemas.py — Pydantic request and response models shared by the handlers in this package.
    • __init__.py — exports only get_router.
  • Optional but common additional files:
    • dependencies.py — FastAPI dependency functions reused across handlers in the package.
    • exceptions.py — custom HTTP exceptions for this domain.

Dependencies

  • Declare reusable dependencies in dependencies.py inside the handler sub-package. Dependencies are plain functions or async functions that return a value needed by endpoints (database session, current user, settings, etc.).
  • Use fastapi.Depends in router.py to wire dependencies to routes. Avoid calling dependencies directly in handlers.
  • Keep dependencies stateless; if they need configuration, accept it as an argument and use a closure or a callable class when wiring.

Example api/rest/users/dependencies.py:

import fastapi

from project.api.rest.exceptions import HTTPUnauthorized
from project.api.rest.dependencies import database


async def current_user(
    database: DatabaseService = fastapi.Depends(database),
    token: str = fastapi.Depends(tokens.oauth2_scheme),
) -> User:
    db_token = await database.get_token(token)
    if db_token is None:
        raise HTTPUnauthorized()
    return await database.get_user(filter_=UserFilter(id=db_token.user_id))

Example api/rest/users/schemas.py:

from pydantic import BaseModel


class UserResponse(BaseModel):
    user_id: int
    requested_by: str

Example api/rest/users/handlers.py consuming a dependency:

import fastapi

from . import dependencies, schemas


async def get_user(
    id: int,
    user: User = fastapi.Depends(dependencies.current_user),
) -> schemas.UserResponse:
    return schemas.UserResponse(user_id=id, requested_by=user.username)

List endpoints

  • Use a shared ListResponse schema for endpoints that return collections. It is defined once in api/rest/schemas.py and reused across packages.
  • Extract pagination and search via a reusable pagination dependency declared in api/rest/dependencies.py.

Example api/rest/schemas.py:

from typing import Any
from pydantic import BaseModel


class ListResponse(BaseModel):
    total: int
    next: str | None
    previous: str | None
    data: list[Any]


class UserResponse(BaseModel):
    ...


class UsersResponse(ListResponse):
    data: list[UserResponse]

Example api/rest/dependencies.py:

import fastapi
from pydantic import BaseModel


class Pagination(BaseModel):
    cursor: str | None = None
    limit: int = 20
    search: str | None = None
    order_by: str | None = None


def pagination(
        cursor: str | None = fastapi.Query(default=None),
        limit: int = fastapi.Query(default=20, ge=1),
        search: str | None = fastapi.Query(default=None),
        order_by: str | None = fastapi.Query(default=None)
) -> PaginationParams:
    return PaginationParams(cursor=cursor, limit=limit, search=search)

Example api/rest/users/handlers.py using the list schema and pagination dependency:

import fastapi

from api.rest.dependencies import PaginationParams, pagination
from . import schemas


async def get_users(
    pagination: PaginationParams = fastapi.Depends(pagination),
) -> schemas.UsersResponse:
    ...

Example api/rest/users/router.py:

import fastapi

from . import handlers


def get_router() -> fastapi.APIRouter:
    router = fastapi.APIRouter()
    router.add_api_route(path="/", methods=["GET"], endpoint=handlers.list_users)
    router.add_api_route(path="/", methods=["POST"], endpoint=handlers.create_user)
    router.add_api_route(path="/{id}", methods=["GET"], endpoint=handlers.get_user)
    router.add_api_route(path="/{id}", methods=["PUT"], endpoint=handlers.update_user)
    router.add_api_route(path="/{id}", methods=["DELETE"], endpoint=handlers.delete_user)
    return router

Example api/rest/users/__init__.py:

from .router import get_router

__all__ = (
    # router
    "get_router",
)
  • Do not construct fastapi.APIRouter instances at import time outside get_router(). The router is created only when get_router() is called during _setup_app().