apiver¶
Define API versions as deltas, not duplicates.
Your existing code becomes the base version, exactly where it already lives. Every later version
declares only what changed against its parent — one override() call — and everything else resolves
straight through to the same handler objects the parent already uses. Not copies. The same objects.
flowchart LR
subgraph V1["v1 — Base Version, 30 routes"]
direction TB
products1["products"]
orders1["orders"]
payments1["payments"]
webhooks1["webhooks"]
addresses1["addresses"]
rest1["24 more routes"]
users1["users"]
end
subgraph V2["v2 — derives from v1, overrides 1 route"]
direction TB
users2[["users — override()"]]
end
classDef overridden fill:#f4a326,stroke:#8a5a00,color:#1a1200,font-weight:bold;
class users2 overridden;
untouched(["the other 29 v2 requests"]) -. "resolve straight through" .-> products1
untouched -.-> orders1
untouched -.-> payments1
untouched -.-> webhooks1
untouched -.-> addresses1
untouched -.-> rest1
changed(["GET /api/v2/users/"]) ==>|"the 1 route v2 actually changes"| users2
v2 mentions exactly one thing: users. Every other route — 29 of them, across 6 resources — resolves
straight through to the exact same v1 handler objects, untouched.
Nothing to reorganize first¶
- Install it.
uv add apiver(orpip install apiver). - Add two settings. Where versions mount, and which ones are live.
- Run
apiver init --base v1. It reads your existing routes and writes the registry for you. - Add one line to your root
urls.py.
That's it. Your existing API is now also live at /api/v1/..., serving the exact same handlers it
always did — nothing moved, nothing got rewritten. Walk through it end to end, commands and output
included →
The problem¶
A Django REST Framework project needs to ship a breaking API change — drop a field, remove a resource, change a type — without breaking the clients still calling the old shape. Every available answer today is bad in a specific way:
- Copy the API into a
v2/package. The 5% that actually changed drags the other 95% along with it. Every later bug fix has to be applied N times, the copies drift silently, and nobody can tell by reading the code which parts of V2 are a deliberate change and which are a stale duplicate that nobody noticed diverging. - Reach for DRF's built-in versioning.
URLPathVersioningsetsrequest.versionand stops there. There's no fallback and no composition, so the developer hand-writesif request.version == "v2":branches scattered through views, serializers and querysets — versioning logic smeared across the codebase instead of declared in one place. Sometimes it's dressed up as a converter function between versions instead of a branch; it's the same disease wearing a different hat — logic that decides what changed, living anywhere except the one place a reviewer would think to look. - Promise to only ever add, never remove. No branch, no override, no version — just pile
status_v2,status_string, andstatus_do_not_usenext to the field nobody had the nerve to schedule a real removal for. Two years in, the response body is an archaeology dig, and no client developer can tell you which field they're actually supposed to read.
None of these give you a way to say "V2 is V1, except payments returns decimal strings and
legacy-invoices is gone" and get a complete, correctly-documented V2 API surface out of it — nor do
they let you answer, at a glance, months later: what does v3 actually serve? Which routes does it
inherit rather than define? Is the published OpenAPI document for v2 still accurate? Did anyone
remember to tell clients v1 is going away?
And none of them require the mess to already exist before it's worth fixing. However your project got here — a single clean version or a decade of hand-rolled ones — apiver only needs to know what your next version changes. See What If...? for the adoption objections this raises in practice.
Philosophy¶
Messy URL patterns are a symptom, not the disease. By the time a project has a views_v2.py, a
serializers_v2_actually_final.py, and three different if version == conditionals guarding the same
queryset, the underlying problem isn't the file layout — it's that nothing in the codebase can say what
changed between versions and what didn't. apiver forces that question to be answered explicitly, once,
at the one place a version's behavior is actually decided: register(), override(), remove().
apiver is also deliberately narrow: three verbs, one direction of inheritance, loud failures on misuse. Nothing tries to be flexible enough to accommodate every way a team might want to version an API; it tries to be narrow enough that there's exactly one obvious way, and it happens to be correct. Read the full Philosophy for the rest of it — including the one place apiver draws a hard line that's what makes squashing a long delta chain mechanical rather than risky.
What you get¶
- A second complete API surface for the cost of one field. One
override()call, andGET /api/v2/products/still works — V2 never mentioned products, and the other 29 routes were never touched. apiver initwraps your project exactly as it is. No file moves, no big-bang migration to schedule. The first breaking change is the only time you touch apiver again.- Deltas that are ordinary, inspectable Python. An override is a subclass. No DSL, no parallel object model, no migration-chain classes to learn — if you can read a Django class hierarchy, you can read a delta.
- Correct per-version OpenAPI, automatically. Each version's schema document contains exactly its own routes — no leakage from siblings, no hand-maintained schema file to keep in sync.
- Lifecycle clients can actually see.
v1.deprecate(sunset=...)emits realDeprecation/Sunsetheaders and enforces410 Goneon the wall clock — no deploy has to land on the date. - Tooling that answers "what does v3 actually serve?"
apiver versionsand a committedapiver.tomlturn that from an archaeology project into a command. - No pretending it can see what it can't. A
SerializerMethodField's actual output, or a changed error-response shape, won't show up in a schema diff —apiver diffsays so out loud instead of quietly missing it. See exactly where the line is →.
Status and roadmap¶
apiver is pre-1.0. The mechanism, verbs, and CLI documented across this site exist and are covered by
tests — none of it is a design document describing something aspirational. What isn't settled yet is
everything below: expect the public API to keep moving until a tagged 0.1 release, and pin an exact
version if you adopt apiver before then.
0.1 is the complete tool, not a minimal one — everything below ships before the first tag, not after it:
- A version-wide config layer. Still a decision
ticket, not a build ticket: whether
Versionshould support version-wide configuration (permission_classes, authentication) that forwards to every route without an explicitoverride()per endpoint. Today's workaround — overriding each affected endpoint explicitly — stays the documented path unless this lands with a real mechanism behind it.
apiver will never relocate your existing files — see Philosophy. apiver init
discovers and imports code from wherever it already lives; that boundary doesn't move as the tool
matures, it's a permanent design decision, not a gap waiting on a --move flag.
1.0 is a stability gate, not a new-feature milestone. It means the public API — the verbs, the CLI, the settings — has held under real adoption without a breaking change, not that new mechanism was added to earn the tag.
Post-1.0, and only once a real second-framework need justifies it — FastAPI, Quart and Litestar
adapters. apiver's public namespace is apiver.drf, not flat apiver, precisely so this stays
possible without a breaking rename later. There is deliberately no apiver.core abstraction layer built
ahead of a second framework actually needing one — building that layer speculatively, before a real
adapter has stressed it, is exactly the kind of premature flexibility Philosophy argues
against.
Where to go next¶
- New to apiver? Start with Getting Started — a real adoption walkthrough, commands and output included verbatim.
- Evaluating whether it fits your project? What If...? answers the specific objections an adoption decision actually raises.
- Want the reasoning, not just the mechanism? Philosophy.
- Looking for a specific command or setting? Reference.