# Versioning Beyond REST: GraphQL, gRPC and Events — Routebase

> Why GraphQL evolves instead of versioning, how Protobuf field numbers make compatibility mechanical, and what a schema registry does for event streams.

Canonical page: https://routebase.dev/guides/api-versioning/versioning-graphql-grpc-and-events/
Chapter 11 of 12 · API Versioning · Last reviewed 2026-09-13 · The Routebase Team

The classification, the additive discipline and the deprecation mechanics in this guide apply beyond REST. The carriers do not, because GraphQL, gRPC and event streams each decided the versioning question in their own way, and the reasoning behind each answer is worth knowing.

## GraphQL evolves rather than versions

A GraphQL client states exactly which fields it wants, and the response contains those fields and nothing else. That single design choice removes most of the reason to version, since adding a field to a type cannot affect a client that did not ask for it.

The result is that most GraphQL APIs are versionless and grow continuously. The documentation on graphql.org describes this as favouring evolution over versioning, with a built-in deprecation mechanism for the cases where a change cannot be additive. Fields carry a `@deprecated` directive with a reason, tooling surfaces it to developers as they write queries, and the field is removed once usage has fallen far enough.

What GraphQL gains in flexibility it pays for in observability. Because every client asks for a different subset, the only way to know whether a removal is safe is to record which client used which field. That measurement is not optional in a versionless API, since it is the thing standing in for a version number.

The breaking changes that remain look familiar. Removing a field or a type, making an optional argument required, changing a field's type, and removing an enum value all break clients exactly as they would over REST.

## Protobuf and gRPC make compatibility mechanical

Protobuf takes the opposite approach and encodes compatibility into the format itself. Fields are identified on the wire by number rather than by name, which produces a small set of rules that are unusually easy to check.

The Protobuf documentation states that a field number cannot be changed once a message type is in use, because the number is what identifies the field on the wire. Removing a field is safe, and the removed number must be reserved so that nobody reuses it later. Renaming is safe for the binary encoding and unsafe for the text and JSON encodings, where the name is serialized. Some type changes are compatible, since `int32`, `uint32`, `int64`, `uint64` and `bool` are interchangeable on the wire, and most other type changes are not.

Because the rules are mechanical, tooling can enforce them at build time, which is why gRPC services often evolve for years without a version number. Where an interface really has to change, the convention is a new package name such as `v2`, which is a version in everything but the transport.

## Event schemas are the hardest case

An event is a message you already sent, and that is what makes streams different. A request and its response are over in milliseconds, while an event sits in a topic or an archive and may be read next year by a consumer that does not exist yet.

Most event platforms handle this with a schema registry that enforces a compatibility mode on every new schema version. The mode is the interesting decision.

| Mode | Means | Fits when |
|---|---|---|
| Backward | New consumers can read old events | Consumers upgrade before producers |
| Forward | Old consumers can read new events | Producers upgrade before consumers |
| Full | Both directions hold | You cannot control the order |

Choosing full compatibility restricts you to genuinely additive change with defaults on every new field, which is exactly the discipline from [backward compatible change](/guides/api-versioning/backward-compatible-api-changes/) enforced by a machine.

Webhooks deserve a mention of their own, because they are a contract you push to somebody else's endpoint and they are easy to forget. Versioning a subscription rather than a request is the usual answer, so a receiver keeps getting the shape it registered for. Stripe pins an API version on each webhook endpoint for this reason, as its versioning documentation describes.

## What carries across

Strip away the transports and the same three ideas survive.

Additive change is always the cheap path, whatever the technology. Deprecation with a date and a replacement is always better than removal, and always requires knowing who is still calling. Compatibility is a property of the running system rather than of the schema, so somebody has to check it.

_Figure: One call drawn for each of the three, with the contract in a different place every time. GraphQL has nothing standing between the parties because it evolves in place and deprecates single fields. gRPC keeps the contract and numbers its clauses, which is what makes its compatibility checks mechanical. An event has no reply at all, so the contract travels inside the payload and the producer never learns who read it._

The rest is a question of which layer enforces the rules for you. Protobuf enforces the most, GraphQL enforces the least and gives you the most room, and REST sits between them and leaves the decision to you.

## In Routebase

Routebase versions OpenAPI specifications, which covers REST and any HTTP API you can describe in OpenAPI. GraphQL schemas, Protobuf definitions and event schemas are not part of that.

The parts of this guide that are about method rather than format still apply to how you work. Classify the change before you decide anything, look for the additive path first, and give every retirement a date and a replacement. Where your HTTP surface is described in OpenAPI, the [Spec Versioning guide](https://docs.routebase.dev/versioning/) covers how that plays out in the product.

## Frequently asked questions

### How do you version a GraphQL API?

Most GraphQL APIs are not versioned at all. Because a client asks for the exact fields it wants, adding a field affects nobody, so the schema grows continuously and fields that are going away are marked with the deprecated directive. The GraphQL documentation describes this as favouring evolution over versioning, with deprecation as the built-in mechanism when a breaking change is unavoidable.

### How does Protobuf handle backward compatibility?

Compatibility in Protobuf is decided by field numbers rather than names, because the number identifies the field on the wire. The documentation states that a field number cannot be changed once a message type is in use, that removing a field is safe, and that the removed number must be reserved so nobody reuses it. Some type changes are compatible and most are not.

### How do you version event schemas?

Event streams usually version through a schema registry that enforces a compatibility mode, so a producer cannot publish a schema that would break existing consumers. Backward compatibility lets new consumers read old events, forward compatibility lets old consumers read new events, and the mode you pick depends on whether producers or consumers upgrade first. Events are harder than requests, because a stored event has to stay readable for as long as you keep it.

### Do webhooks need versioning?

Yes, and they are easy to forget, since a webhook payload is a contract you push to somebody else's endpoint. The usual approach is to version a subscription rather than a request, so an existing receiver keeps getting the payload shape it registered for while new receivers get the current one. Stripe pins the API version on each webhook endpoint for exactly this reason.

---

[Routebase](https://routebase.dev/) — [Sign up](https://app.routebase.dev/): Every account starts with a 14-day Pro trial — no credit card required.
