Lesson 21 of 32 intermediate 5 min read

Before this:API contractsgRPC & Protocol Buffers

Schemas & code generation

Key takeaways A schema is the API contract written in a machine-readable form — a .proto file, an OpenAPI document — from which tools can generate typed clients, server scaffolding, docs, and validators. The payoff is a single source of truth: the contract can’t silently drift from the code, and a breaking change becomes a visible diff in one file. The discipline that makes it work is contract-first: the schema leads, code follows — never the reverse regenerated from whatever the code happens to do.

Unit 4 closes by generalising its best idea. gRPC’s .proto workflow — define once, generate everywhere — isn’t a gRPC quirk; it’s a philosophy that applies to REST too, and it directly attacks the failure mode Unit 1 warned about: contracts that live only in prose and habit, quietly rotting.

The problem: contracts written in prose drift

Without a schema, your API’s contract exists in several places at once: the server’s handler code, the docs page, each client’s parsing code, and everyone’s memory. Nothing enforces agreement among them. The server adds a field, the docs lag, a client typos talkgorup and reads null forever — remember that a mistyped JSON key doesn’t error, it just yields nothing. Every one of those drifts is invisible until a bug surfaces far away.

A schema collapses them into one artifact. For gRPC you’ve seen it — the .proto file. For REST APIs the standard is OpenAPI (née Swagger): a YAML/JSON document describing every path, method, parameter, and response shape:

paths:
  /api/v1/talkgroups/{id}:
    get:
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: integer }
      responses:
        "200":
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Talkgroup"
components:
  schemas:
    Talkgroup:
      type: object
      required: [id, label]
      properties:
        id:       { type: integer }
        label:    { type: string }
        priority: { type: integer }

What generation buys

Feed that file to the toolchain and out comes, per language you care about:

  • Typed clientsGetTalkgroup(id int) (Talkgroup, error) instead of hand-parsed maps; the typo bug becomes a compile error.
  • Server scaffolding — handler interfaces the implementation must satisfy, so the server can’t accidentally diverge from the contract either.
  • Validators — request/response checking against the schema at runtime or in tests.
  • Documentation — rendered reference pages that are definitionally current, the foundation the API documentation lesson builds on.

And one more thing, subtler and maybe most valuable: reviewable evolution. When the contract is a file, every change to it is a diff in version control. “Wait, this PR renames a response field — that’s breaking” becomes a code-review observation instead of a production incident. Tools can even lint schema diffs for compatibility automatically, mechanising the additive-change discipline.

Contract-first, not code-first

There are two ways to hold a schema, and they differ more than they look:

  Contract-first Code-first
The schema is authored — the design artifact extracted — generated from code annotations
Design pressure interface thought through before implementing whatever shape the code grew
Drift risk code can’t drift (it’s generated/validated) schema faithfully documents drift as it happens

Code-first (“annotate the handlers and export OpenAPI”) feels convenient and is better than nothing — but it inverts authority: the code leads, and the schema becomes a description rather than a contract. Every accidental change of shape gets dutifully exported as the new “contract.” Contract-first keeps the promise where Unit 1 said it belongs: designed deliberately, changed deliberately.

Rule of thumb: the schema is the contract’s home. Author it, review changes to it like the interface decisions they are, and generate everything else from it.

The honest costs

Schema-first isn’t free, and pretending otherwise breeds backlash. There’s a toolchain to install and version (protoc, generators, CI steps); generated code can be bulky or unidiomatic, and you must never hand-edit it (edits die at next generation — wrap it instead); and schema languages have expressive limits — “field B is required when A is set” often lives in prose or validators anyway, so the schema-plus-docs pairing remains a pairing. For a three-endpoint hobby API, a well-written README and disciplined tests may honestly be the right size. The technique earns its overhead as endpoints, clients, and contributors multiply — which real projects tend to do.

Quick check: what's the core weakness of generating your schema from code annotations?

Recap

  • A schema (.proto, OpenAPI) is the contract in machine-readable form — one artifact where prose-and-habit contracts were many.
  • Generation yields typed clients, server scaffolding, validators, and ever-current docs — and mistyped-field bugs become compile errors.
  • A schema in version control makes evolution reviewable and lintable: breaking changes show up as diffs.
  • Contract-first keeps the schema authoritative; code-first merely documents drift.
  • Costs are real — toolchain, generated-code ergonomics, expressive limits — and scale with project size in reverse: the bigger the project, the better the deal.

Next up: Designing a good API.