Skip to content

Project Structure & Component Cleanliness

FastKitty is organized around a simple architectural goal: clean separation of concerns with zero hidden magic.

Every component has one responsibility, depends only on what it needs, and can be enhanced, extended, and unit-tested in complete isolation.


📁 Repository Layout

fastkitty/
├── main.py                     # FastAPI application entrypoint & lifespan
├── api/
│   ├── routes/
│   │   └── v1/
│   │       ├── hello.py        # Dynamic greet endpoint
│   │       └── blog_posts.py   # Multi-tenant blog CRUD routes
│   └── deps/
│       ├── tenancy.py          # Tenant resolution & feature config dependencies
│       ├── db.py               # Database session & service injection
│       └── user_data.py        # User identity resolution (headers or JWT)
├── services/
│   └── blog_posts_service.py   # Pure business logic (transport & strategy-agnostic)
├── models/
│   ├── base.py                 # Declarative Base, TenantScopedModel, TimestampedModel
│   └── posts.py                # BlogPost entity
├── schemas/
│   ├── posts.py                # Pydantic request & response models
│   ├── tenancy.py              # TenantConfig, FeatureConfig, SecretConfig
│   └── user_data.py            # UserData identity schema
├── config/
│   ├── settings.py             # Application environment settings (Pydantic BaseSettings)
│   ├── tenancy_config.py       # Tenancy config provider interface & loader
│   ├── tenancy_secrets.py      # Tenancy secrets provider interface & loader
│   └── logfire_config.py       # Pydantic Logfire telemetry setup
├── db/
│   ├── engine.py               # Strategy-aware engine factory & LRU connection pools
│   └── session.py              # AsyncSession generator per tenant
├── alembic/                    # Multi-tenant migration scripts
│   └── env.py                  # Multi-database & multi-schema migration runner
├── tenants_config.json         # Local tenant feature flags & metadata
└── tenants_secrets.json        # Local tenant DB connection secrets

Why Components are Clean & Lean

Component Responsibility What it Knows What it NEVER Knows
Route Handler (api/routes/) HTTP deserialization, status codes, response shapes Pydantic schemas, dependency signatures SQL queries, rate limiting algorithms, DB strategy
Dependency Layer (api/deps/) Resolving request context, auth, sessions FastAPI Request, config providers, DB engine Business rules, response formatting
Service Layer (services/) Business workflows, domain rules, transactions Domain models, AsyncSession HTTP requests, cookies, headers, status codes, DB strategy
Model Layer (models/) Database schema definition, column types, relationships SQLAlchemy Declarative Base How sessions are acquired or who the current tenant is
Config & Secrets (config/) Loading credentials and feature flags from storage JSON files, Vault, Consul, Redis Routes, services, or business logic

How to Enhance & Extend Each Component

1. Extending the Service Layer

Services only take an AsyncSession in their __init__. To add business logic: - Add a new method in services/. - Use standard SQLAlchemy async queries. - Because services are decoupled from HTTP, you can call them from background workers (Celery, ARQ), CLI scripts, or async queues with no changes.

2. Extending Dependencies

If you need new context (e.g. tenant billing status, user roles): - Add a helper function in api/deps/. - Use Depends() to compose dependencies naturally.

3. Swapping Infrastructure Providers

FastKitty uses interface protocols for config and secrets. To switch from local JSON files to HashiCorp Vault or AWS Secrets Manager: - Subclass the provider base class in config/. - Update TENANCY_SECRETS_PROVIDER in your .env. - Not a single line of application code or route code changes.


Testing Made Effortless

Because components have explicit seams, testing is fast and deterministic:

1. Unit Testing Services (Zero HTTP, Zero Network)

Test your business logic by passing an in-memory SQLite session directly to the service:

async def test_create_post_enforces_quota(async_session):
    service = BlogPostsService(async_session)
    # Test business logic directly
    post = await service.create_post(..., feature_config={"max_daily_posts": 1})
    ...

2. Integration Testing API Routes

Override FastAPI dependencies using app.dependency_overrides without needing real cloud services:

app.dependency_overrides[get_tenant_config] = lambda: mock_tenant_config
app.dependency_overrides[get_db_session] = lambda: test_session

Next Step

Want to see how all these components come together from scratch? Follow our end-to-end guide:

👉 How to Add a New Route & Table: Complete End-to-End Walkthrough