API versioning policy
Documenting the rule from day one so consumers can build with confidence.
Promise
/v1 is permanent. parcelpump will never:
- Remove a
/v1/...endpoint. - Change an existing
/v1/...endpoint's URL. - Reshape a
/v1/...response in a way that breaks existing consumers (renaming a field, changing its type, removing a field). - Add a required request parameter to a
/v1/...endpoint. - Tighten validation on a
/v1/...request (e.g., shrink an accepted enum, add a stricter regex).
What we can do within /v1 (additive only):
- Add new endpoints under
/v1/.... - Add new optional request fields / query params.
- Add new fields to response objects (consumers must ignore unknown fields — standard REST practice).
- Add new entries to enums where the consumer is documented as treating unknown enum values as "other / unknown".
- Improve internal performance, reliability, observability — none of which is observable to the contract.
If a change would break the rule, it goes into a new major version
(/v2/...) instead.
What's versioned vs what isn't
Versioned (under /v1/...):
- Every data endpoint: parcels, sources, search, findings, scrape-jobs, admin-concurrency, admin-sources/health, etc.
Unversioned (no prefix; describes the service not the contract):
/health— liveness check; same shape forever/openapi.json,/docs— the spec itself; describes the latest released version by default/llms.txt,/.well-known/ai-plugin.json— discovery/tiles/manifest.jsonand/tiles/<layer>/<hash>.pmtiles— the PMTiles archive shape is dictated by the PMTiles format, not by us
When v2 ships
If/when a breaking change becomes necessary, the launch sequence is:
- Implement v2 alongside v1. Both available simultaneously at
/v1/...and/v2/.... Same Lambda, same DB; different route handlers. - Update the OpenAPI spec.
/v2/openapi.jsondocuments v2;/v1/openapi.jsonis added (the bare/openapi.jsondefaults to the latest). - Announce the deprecation. Email every active consumer key,
note in
/llms.txtand the changelog, setSunsetandDeprecationHTTP headers on every v1 response. - Wait at least 12 months. v1 stays fully functional during this window. New features land in v2 only.
- Sunset v1. v1 endpoints return
410 Gonewith a body pointing at the v2 equivalent. They are NOT deleted from the registry — the response just becomes a redirect/error so old code fails loudly rather than silently producing wrong data.
Internal mechanics
- Routes live under
app.METHOD("/v1/...", handler)insrc/api/server.ts. Adding a v2 route addsapp.METHOD("/v2/...", handler)(often delegating to a shared internal function). src/api/route-registry.tscarries the version-prefixed path of every route. Each entry's path begins with/v1/. v2 entries begin with/v2/.- The OpenAPI generator includes both versions. Swagger UI shows both grouped by tag.
docs/api-reference.md(generated) lists both versions side by side.- The MCP server defaults to v1; a future PR can add a per-tool
apiVersionparameter when v2 ships. farminggame/src/lib/parcelpump-client.tsuses/v1/...URLs hardcoded. When v2 ships, the client adds new methods (or a wrapping abstraction) — the existing methods stay v1 forever.
Consumer guidance
- Always include the version in your URL. Don't try to be clever with content-negotiation or default-version behavior. URL versioning is debuggable and grep-able.
- Treat unknown response fields as ignorable. If we add
parcels[*].new_fieldin v1, your v1 client must not reject it. - Pin the SDK / generated client to a major version. If we publish
@parcelpump/client@1.x, that line never gains breaking changes. v2 ships as@parcelpump/client@2.xand you opt in by upgrading.
Rate limits + uptime stay version-agnostic
Rate limits (per key) and uptime guarantees (none formally yet) apply across versions equally. The version prefix is about the shape of requests and responses, not the availability of the service.
Why URL-based versioning
Tradeoffs we considered:
- URL (
/v1/parcels/...) — chosen. Most explicit, debuggable viagrep/curl, works trivially with CloudFront caching and Swagger UI groupings. - Header (
Accept-Version: v1) — cleaner URLs but harder to debug, requires extra-Hflag for ad-hoc curl, less obvious in logs, doesn't compose with browser<a href>links. - Query param (
?api-version=1) — mixes versioning into the filter parameters; awkward in Swagger UI; bad UX for sharing URLs. - Subdomain (
v1.api.parcelpump.io) — requires a separate CloudFront distribution per major version, separate ACM cert per hostname, extra DNS — meaningful operational tax.
URL versioning costs nothing extra and has no real downsides at parcelpump's scale.