Query parameter versioning
Version as a query parameter
There’s a third approach, and it’s the simplest one: add the version as a query parameter.
GET /books?version=2
GET /books/book-1?version=2
POST /books?version=1 No special URL paths. No headers. Just a parameter.
Implementation
route.get("/books", {
request: {
query: z.object({
version: z.coerce.number().int().default(1),
}),
},
resolve: (c) => {
if (!c.input.ok) return Response.json({ error: c.input.issues }, { status: 400 });
const { version } = c.input.query;
if (version === 2) {
return getV2Books();
}
return getV1Books();
},
}); The handler reads version from the validated query. Zod coerces the string "2" into the number 2 and defaults to 1 when the parameter is missing. If it’s 2, serve the v2 response. Otherwise, default to v1.
The advantages
Easiest to test. Paste the URL in a browser with ?version=2. No curl flags, no header configuration, no special tools. Anyone can test it immediately.
Visible in URLs. Like path versioning, you can see the version in the URL. Unlike path versioning, it doesn’t change the path structure.
Easy default. Omit the parameter and you get v1. There’s no ambiguity about what happens when the version is missing.
The disadvantages
Easy to forget. Clients have to remember to include ?version=2 on every single request. What do you think happens if they forget it on one endpoint? They get a v1 response mixed in with their v2 responses. That’s a subtle, hard-to-debug inconsistency that can cause real problems.
Pollutes query parameters. The version mixes with actual query parameters: /books?version=2&genre=fiction&page=1. The version isn’t data. It’s metadata about which API shape to use. It doesn’t really belong alongside filters and pagination.
Caching issues. /books?version=1 and /books?version=2 are technically different URLs, which is good for caching. But /books (no version) is a third URL that defaults to v1. Ideally that should share v1’s cache, but it won’t because the URLs are different.
Not conventional. Most APIs use path versioning. Developers encountering your API expect /v2/books, not /books?version=2. Unconventional choices create friction, and friction means more support questions.
When query parameter versioning makes sense
It’s not all bad. There are specific situations where this approach works well:
Internal APIs where all clients are under your control and you can enforce the parameter consistently.
Experimental features where you want to test a new response format: /books?format=enhanced.
Temporary A/B testing of API response shapes before committing to a full new version.
For public APIs with external consumers, the risks usually outweigh the convenience.
We’ve now seen all three versioning strategies. In the next lesson, we’ll put them side by side and make a decision.
Exercises
Exercise 1: Add version as a query parameter. Default to 1 when omitted. Test with and without the parameter.
Exercise 2: Call /books?version=2&genre=fiction. Verify both the version and genre parameters work correctly.
Exercise 3: Forget the version parameter on one endpoint but include it on another. Observe the mixed v1/v2 responses.
Why is query parameter versioning rarely used for public APIs?