I built QuadKit alone. No team, no funding, no timeline. Just a problem I kept hitting — Python web frameworks don’t think in protocols, don’t enforce boundaries, and don’t give AI coding agents the machine-readable context they need to generate correct code.
This post explains the architecture decisions, the agent-augmented workflow, and why the project looks the way it does.
The problem
Section titled “The problem”Every Python web app I built ended up the same way: services importing from each other, Result types leaking into controllers, and no way to swap implementations without touching three files. FastAPI made it worse — Pydantic models double as domain types, so your API layer and business logic fuse together.
The inconsistency problem
Section titled “The inconsistency problem”Pick any two Python web projects and you’ll find different patterns for the same thing. Not minor style differences — structural inconsistencies that break the mental model:
-
Error handling. One module raises
ValueError("user not found"), another returnsNoneand checks at the call site, another wraps in a custom exception with a string message. There’s no contract for what a failed operation looks like, so every caller invents its own error-checking pattern. -
Dependency wiring. One service takes its dependencies as
__init__arguments, another uses@injectdecorators, another pulls from a globalsettingsobject. When you need to swap a mock in tests, you have to figure out which pattern each service used. -
Configuration access. One module reads
os.environ["DB_URL"], another usespydantic-settings, another takes a config dict passed through three layers. There’s no single place to see what the app needs or validate that it’s complete. -
Project layout.
models/vsdomains/vsschemas/vstypes/.services/vshandlers/vsuse_cases/.controllers/vsroutes/vsviews/. Every project invents its own directory grammar, and contributors have to learn it from scratch. -
API response shape. One endpoint returns
{"data": [...], "meta": {...}}, another returns a flat list, another returns{"items": [...], "total": 5}. Clients parse three different shapes from the same API. -
Type hints. One file uses
Optional[str], another usesstr | None, another usesAnybecause “it works anyway.” Protocols vs ABCs. TypeVar vs generic. The type system is there but used inconsistently, so static analysis catches half the problems. These aren’t cosmetic. They compound: -
Onboarding takes longer. New contributors learn project-specific conventions instead of transferable skills. The patterns don’t generalize — they’re local trivia.
-
Code review becomes subjective. “This isn’t how we do it here” replaces objective rules. Without enforceable patterns, style guides are suggestions.
-
AI agents generate wrong code. An agent trained on one project’s patterns will produce inconsistent output in another. The agent doesn’t know which convention applies.
-
Refactoring is risky. Without enforced boundaries, changing one module silently breaks three others. The dependency graph is implicit and unverifiable.
QuadKit solves this by making the patterns enforceable, not aspirational. The import linter doesn’t care about your opinion — it checks the rule. The contracts package doesn’t suggest protocols — it requires them. The error model doesn’t offer a choice — it returns Result[T, E].
I wanted:
- Contracts first. Protocols live in a zero-dependency package. Everything else depends on them.
- Typed errors.
Result[T, E]instead of exceptions for domain failures. 240+ error codes, one pattern. - Import linting. A hard CI gate that blocks cross-extension imports. Not a convention — a rule.
- Agent-native docs.
/llms.txt,/agents.md,/SKILL.md— files that coding agents load directly, not scraped from HTML.
The hierarchy
Section titled “The hierarchy”quadkit-contracts Zero deps. Protocols, types, exceptions only. ↑quadkit Depends ONLY on quadkit-contracts. ↑quadkit-* Never import each other.This is the whole design. If two packages need the same type, it lives in quadkit-contracts. No exceptions. An import linter enforces it — a cross-extension import that type-checks still fails CI.
Why? Because every violation I allowed in past projects became a maintenance tax. The linter makes it impossible to accumulate.
The DI container
Section titled “The DI container”Most Python DI frameworks use decorators or globals. I built a provider-based system:
- Providers register bindings (
register) and resolve them (boot). Two phases, never mixed. - Modules group related providers.
WebModule,CacheModule,TaskModule. - The container is a plain dictionary under the hood. No magic, no thread-local, no singleton pattern.
from quadkit.di.provider import Providerfrom quadkit.contracts.core import ProviderPriorityfrom quadkit.contracts.core.di import ContainerRegistrarProtocol, BootContainerProtocol
class BillingProvider(Provider): name = "billing" priority = ProviderPriority.APPLICATION
async def register(self, container: ContainerRegistrarProtocol) -> None: container.singleton(PaymentGateway, StripeGateway(cfg.stripe_key)) container.singleton(InvoiceRepository, InvoiceRepository(cfg.db_url))
async def boot(self, container: BootContainerProtocol) -> None: gateway = await container.resolve(PaymentGateway) repo = await container.resolve(InvoiceRepository) container.singleton(PaymentService, PaymentService(gateway, repo))This is testable. You can swap providers in tests without patching globals.
Agent-augmented development
Section titled “Agent-augmented development”I built QuadKit with AI coding agents — Claude Code, OpenCode, Cursor. Not as a gimmick, but because solo development means every hour counts.
The workflow:
- I write the core logic and tests.
- I delegate boilerplate (controllers, services, error codes) to agents.
- Agents load
/llms.txtand/agents.mdto understand the hierarchy, then generate code that follows the rules. - The import linter catches any violations. The type checker catches the rest.
The key insight: agents need stable, machine-readable context. Not blog posts, not READMEs scattered across repos. One file that lists every URL, every rule, every package. That’s /llms.txt.
The docs site
Section titled “The docs site”This site (oridecon.dev) is built with Astro + Starlight. It serves two audiences:
- Humans — guides, examples, the framework landing page.
- Agents —
/llms.txt,/agents.md,/SKILL.md, per-package indexes at/llms/<package>.txt.
The same content, two consumption paths. The agent files are generated from the same source as the human docs, so they never drift.
The numbers
Section titled “The numbers”- 590+ protocols across the core and extensions.
- 240+ error codes with the
QK_ERR_*prefix pattern. - 5 published packages:
quadkit,quadkit-contracts,quadkit-web,quadkit-cli,quadkit-testing. - 80%+ test coverage as a CI gate.
- 40+ in-house packages powering three MVP projects.
- Apache 2.0 — every line is public.
What’s next
Section titled “What’s next”The framework is at v0.0.4 alpha. The architecture is stable. The next milestone is v0.1.0 — the first release with a stable public API, full test coverage across all packages, and production-ready docs. After that, v0.2.0 adds the extensions that power real workloads: AI, events, workflows, and caching.
If you’re building AI backends and want to try the framework, start with the examples or read the agent playbook.