How do you keep GraphQL contract review meaningful when the schema is derived from code?
answer
- Nobody wrote it, so nobody reviewed it
- A code diff hides a contract change
- Give the surface a file of its own
- Design-first is not the same as schema-first
- Size the process to the blast radius
basics
~20 sGive the derived schema a committed artefact so surface changes show up in review, agree the shape in SDL before it is built, and name an owner for the shared graph's conventions rather than assuming the implementing team reviews the contract.
solid answer
~50 sThe problem is organisational before it is technical: in code-first nobody writes the API, so nobody reviews it as an API — the pull request shows a code diff and the surface change rides along inside it. Three moves fix that. **Print and commit the schema** so every merge that moves the surface produces a readable diff a reviewer can react to. **Separate design from implementation**: agree the shape in SDL first, as a design artefact, even when the server is built code-first — design-first and code-first are orthogonal. **Assign ownership** of the shared graph's conventions, so naming, nullability and pagination shape are reviewed by someone accountable for the whole surface rather than by whoever happened to review the service. The tradeoff is real: each move adds ceremony to a workflow chosen for having none.
code
pseudocode · 8 lines// build step: the contract gets a location
executableSchema = assembleSchemaFromCode()
printed = printSdl(executableSchema) // not from introspection:
// that loses directive applications
writeFile("graphql/schema.graphql", printed)
// the committed file is what review reads, and what
// consuming teams are handed when they ask for the contractgo deeper
Understand that in code-first the API surface is not written down anywhere by default. If your change adds or renames something clients can see, call that out explicitly in the pull request rather than assuming a reviewer will spot it.
Be ready to explain why a printed, committed schema file is worth a build step: it turns an invisible surface change into a readable diff. Know that the printed file is an output and that editing it changes nothing.
Show how you would introduce this into a team that has none of it, in cost order — print and commit first, pin exposed names second, design artefacts only where consumers justify them — and be able to say what each step actually caught.
Own the coherence of the whole graph: who is accountable for naming and nullability across services, which conventions are linted rather than argued, and how much process each blast radius earns. Be equally ready to say where the answer is to authorise less ceremony, not more.
## The real problem: an unwritten contract has no reviewer Code-first is chosen because it removes an artefact and a binding step. What it also removes is the moment where somebody looked at the API and said yes. In a schema-first repository, a change to the public surface is *necessarily* a change to a file whose entire content is public surface, and any reviewer opening that diff is, at that instant, doing API review. In a code-first repository the same change is a few lines inside a class, surrounded by logic, tests and imports. The reviewer is doing code review, and doing it well, and the contract change is not what they are looking at. At one team and one service this costs little. On a graph several teams contribute to, it compounds: every service invents its own words for the same idea, nullability is decided by whichever return type was convenient, pagination is shaped differently in three places, and the graph stops feeling like one product. None of that is caught by a good code reviewer, because none of it is visible in a code diff. ## Move one: give the contract a location Print the assembled schema on every build and commit the output. This is the cheapest intervention and the one to do first. It converts an invisible change into a visible one — a removed field, a tightened nullability, a new enum value all appear as a diff in the language of the API, in a file whose only subject is the API. Two details are worth getting right. Print from the **executable schema**, not from an introspection result, because introspection does not expose applications of custom directives and a schema reconstructed from it silently loses them. And make regeneration part of the build rather than a step someone remembers, or the file rots into a document that describes last quarter's API and is trusted anyway. The mechanics of comparing that artefact automatically and deciding what to do about a difference are a separate discipline; the point here is simply that until the artefact exists there is nothing to compare or to read. ## Move two: separate designing the schema from building it The deeper fix is to stop treating the schema as an output of implementation. **Design-first is orthogonal to code-first**: you can write SDL as a proposal, argue about it, get the consuming teams to agree it, and then implement it with a code-first server, printing the result and checking it matches what was agreed. The SDL in that workflow is a design document, not a runtime input, which is precisely why this hybrid is popular — it recovers the review artefact without reintroducing the binding drift that schema-first carries. The cost is a maintained comparison and a real process step. It is worth it in proportion to how many teams consume the field and how expensive it is to change your mind later — which is why a shared, externally consumed graph justifies it and an internal service owned by its only consumer usually does not. ## Move three: name an owner for the surface Even with an artefact and a design step, someone has to hold the line on the things that make a graph coherent: naming, nullability policy, mutation shape, how pagination looks, whether an identifier is opaque. A common structure is a small group that reviews schema changes across services — not to gatekeep every merge, but to own written conventions and to be a required reviewer on the printed schema file. Codifying the mechanical part of those conventions in a lint pass keeps the human review for the judgement calls. The failure mode to avoid is a review board with no artefact: asking a group to review changes they can only see by reading server code guarantees the review becomes a rubber stamp. ## The tradeoff to state out loud Everything above adds ceremony back to a workflow that was picked for having none, and a principal answer should own that rather than pretend the fixes are free. A useful framing is to size the process to the blast radius: * **One team, one consumer, internal.** Code-first with derived names and no printed schema is fine. The consumer is in the room. * **Several internal consumers.** Print and commit the schema, and pin the names of exposed members explicitly so refactors cannot move them. That is nearly all the value for nearly none of the cost. * **A shared graph, or external consumers.** Design the schema before building it, review it as an artefact, and give the conventions an owner. Here a mistake is permanent in a way it is not internally, because you cannot redeploy the callers. And be willing to say when the answer is to switch. A service whose contract is argued over more often than its implementation changes is telling you the schema, not the code, is the thing being designed — and that is the case where authoring SDL by hand stops being ceremony and starts being the point.
- Why print from the executable schema rather than from an introspection result?Introspection does not expose applications of custom directives, so a schema reconstructed from an introspection result silently drops them. Printing from the assembled schema keeps everything the server actually holds. Introspection-based printing is fine when you can only reach a running server, but it should be understood as lossy rather than as the canonical form.
- Where would you not bother with any of this?A service with one consumer owned by the same team, deployed together. The contract is being reviewed continuously by the only people affected, and adding a design artefact and an owner buys coordination nobody needs. The moment a second team, or a client you cannot redeploy, depends on the surface, the calculation changes.
- How do you tell that a service should move from code-first to authoring SDL by hand?When the schema is argued about more often than the implementation changes. That is the signal that the contract is the artefact being designed, and the team is repeatedly reconstructing a design conversation from code. At that point writing the SDL is not ceremony added to the workflow — it is the workflow, and the code becomes what follows from it.
- What does a schema lint pass buy that human review does not?It makes the mechanical conventions — naming style, required descriptions, pagination shape, banned patterns — non-negotiable and instant, so review time goes to the judgement calls instead of to style arguments. It also applies uniformly across teams, which is exactly where a human reviewer's attention is least consistent.
saying these in an interview costs you the question
- Assuming careful code review catches surface changes
- Treating design-first and schema-first as the same thing
- Standing up a review board with no schema artefact
- Printing the schema by hand instead of in the build
- Applying full governance to a single-consumer service
- Calling the derived schema self-documenting and stopping there