How GitHub Built Their v4 API
When GitHub launched its v4 API in 2016, it picked GraphQL and committed to GraphQL schema-first design as its guiding philosophy. Instead of letting backend code generate the schema automatically (the code-first path), GitHub's team designed the schema as a contract first, sitting down with frontend engineers, mobile teams, and integration partners before writing resolvers. The result was an API that felt natural to the people consuming it rather than one that just mirrored GitHub's internal database tables.
You can see this in the details. The Repository type exposes stargazers, not stars_count. It has pullRequests with filter arguments instead of a REST endpoint like /repos/:id/pulls?state=open. And collaborators is a proper connection type, not a raw array someone has to paginate by hand. This schema-first philosophy is a big part of why GitHub's GraphQL API is still held up as one of the more developer-friendly ones in production, handling billions of queries a month.
If this sounds familiar, it's the same instinct behind designing your low-level building blocks before touching a database schema; see our piece on the building blocks of low-level design for the same idea applied one layer down.
Schema-First vs Code-First
Two Approaches to Building GraphQL APIs:
CODE-FIRST (bottom-up):
+-----------+ +-----------+ +-----------+
| Database | --> | Resolvers | --> | Schema |
| tables | | (code) | | (generated)|
+-----------+ +-----------+ +-----------+
Problem: Schema mirrors your DB, not client needs.
Example: User.created_at_timestamp (DB column name leaks)
User.user_status_id (foreign key leaks)
SCHEMA-FIRST (top-down):
+-----------+ +-----------+ +-----------+
| Client | --> | Schema | --> | Resolvers |
| needs | | (contract)| | (implement)|
+-----------+ +-----------+ +-----------+
Result: Schema serves the consumer.
Example: User.joinedAt (readable name)
User.status (enum: ACTIVE | SUSPENDED | DELETED)
Aspect Code-First Schema-First
--------------- ---------------- ----------------
Who drives it? Backend devs Frontend + Backend
API quality DB-shaped Consumer-shaped
Parallel dev Sequential Frontend starts immediately
Breaking changes Easy to introduce Schema review catches them
Tooling Introspection Schema file is documentationThe table row that matters most in practice is "parallel dev." Code-first forces frontend teams to wait until resolvers exist before they can build against real shapes. Schema-first flips that, once the contract is agreed on, both sides start immediately, which is the whole point of GraphQL API design done well.
Designing Good Schemas
Schema Design Principles:
1. USE DOMAIN LANGUAGE, NOT DB LANGUAGE
BAD: User.full_name_varchar (DB column)
GOOD: User.fullName (domain term)
2. DESIGN MUTATIONS AROUND BUSINESS OPERATIONS
BAD: updateOrder(id, status: 'shipped') (CRUD)
GOOD: shipOrder(id, trackingNumber: '...') (business action)
Why: Business mutations encode rules (e.g., can't ship cancelled order)
3. MAKE NULLABILITY INTENTIONAL
BAD: type User { name: String } # nullable by default - WHY?
GOOD: type User { name: String! } # non-null - always has a name
type User { bio: String } # nullable - user may not set bio
4. USE CUSTOM SCALARS FOR DOMAIN TYPES
BAD: createdAt: String # '2024-01-15'? '1705276800'? ISO format?
GOOD: createdAt: DateTime # self-documenting, validated
5. INPUT TYPES FOR MUTATIONS
BAD: createUser(name: String!, email: String!, role: String!)
GOOD: createUser(input: CreateUserInput!): CreateUserPayload!
Example Well-Designed Schema:
type Query {
viewer: User! # current authenticated user
repository(owner: String!, name: String!): Repository
search(query: String!, first: Int): SearchResultConnection!
}
type Mutation {
createRepository(input: CreateRepoInput!): CreateRepoPayload!
starRepository(id: ID!): StarRepoPayload!
# NOT: updateRepository(id, starred: true) <-- CRUD thinking
}Rule three trips people up the most. Nullability in GraphQL isn't a formality, it's a promise to every client that consumes the field. If name is nullable "just in case," every frontend engineer now has to write null-check code for a field that, in reality, always has a value. Be deliberate about it, and treat every ! or missing ! as a decision you're making on purpose, not a default you forgot to change.
Schema as a Development Contract
Parallel Development Workflow:
Day 1: Frontend + Backend agree on schema
| |
Day 2: Frontend builds UI Backend builds resolvers
using mock resolvers against real data sources
(schema is the contract) (schema is the target)
| |
Day 5: Integration |
Mock resolvers replaced |
with real GraphQL endpoint |
|________________________________|
Works immediately because both
followed the same schema contract.
Schema Review Checklist:
[ ] Does every type use domain language?
[ ] Are nullable fields intentionally nullable?
[ ] Do mutations represent business operations?
[ ] Are pagination fields using connections (not arrays)?
[ ] Do error responses use union types?
[ ] Is the schema backward-compatible with v(N-1)?This is where schema-first design earns its keep on a real team. Once the contract is written down, the frontend can build against mock resolvers on day two instead of waiting around for the backend to finish its data layer. If you've worked with contract-first tooling on the async side, this will feel familiar, it's the same principle behind AsyncAPI, CloudEvents, and the transactional outbox pattern, just applied to request/response instead of events.
GraphQL is also only one of several ways to shape the contract between clients and services. If you're weighing it against REST gateways, BFFs, or tRPC, it's worth reading our rundown of API Gateway, BFF, tRPC, and the multi-protocol architecture before committing to one pattern across a whole org.
Interview Tip
When discussing GraphQL API design, say: "I prefer schema-first design because it forces the API to serve client needs rather than mirror backend structure. The schema becomes a contract that enables parallel development, frontend and backend teams work independently against the same contract. I design mutations as business operations (shipOrder, not updateOrderStatus) and use non-null types intentionally. Before merging schema changes, I run a backward-compatibility check to ensure existing clients are not broken." Mentioning GitHub's v4 API as a schema-first success story adds credibility.
Architecture overview
Key Takeaway
Schema-first design puts the API contract at the center of development. Design the schema collaboratively with frontend teams using domain language, business-operation mutations, and intentional nullability. The schema becomes documentation, contract, and parallel-development enabler all at once. Done well, this approach produces consumer-friendly APIs that outlast any backend refactor.