Schema-First vs Code-First
Every GraphQL server has a schema and a set of resolvers. The question this lesson answers is: which one do you write first, and which one is the source of truth? There are two answers, and the choice colours your whole codebase.
Schema-first — the SDL leads
Section titled “Schema-first — the SDL leads”In schema-first development you write the schema in SDL — the same type, Query, and ! syntax you learned in earlier modules — and treat that text as the single source of truth. The resolvers are written separately and attached to that schema by name. The SDL file is the contract; the resolvers are the implementation that fulfils it.
type Query { hello(name: String!): String!}const typeDefs = /* GraphQL */ ` type Query { hello(name: String!): String! }`;
const resolvers = { Query: { hello: (_parent: unknown, args: { name: string }) => `Hello, ${args.name}!`, },};The strength is that the contract is front and centre. Anyone — frontend, backend, a non-engineer — can read the SDL and know exactly what the API offers. The risk is drift: nothing in plain SDL forces your resolver’s return type to match the field’s declared type. A resolver can quietly return the wrong shape, and you only find out at runtime. That gap is exactly what the next lesson’s code generation closes.
Code-first — the code leads
Section titled “Code-first — the code leads”In code-first development you build the schema with your programming language. You construct type objects in code, and the SDL is generated from those objects. The code is the source of truth; the SDL is an artifact you can print out.
import { GraphQLObjectType, GraphQLSchema, GraphQLString, GraphQLNonNull,} from 'graphql';
const QueryType = new GraphQLObjectType({ name: 'Query', fields: { hello: { type: new GraphQLNonNull(GraphQLString), args: { name: { type: new GraphQLNonNull(GraphQLString) } }, resolve: (_parent, args: { name: string }) => `Hello, ${args.name}!`, }, },});
const schema = new GraphQLSchema({ query: QueryType });Here the field, its type, its arguments, and its resolver live together in one object. The compiler checks them as a unit, so the drift problem mostly disappears — a resolver that returns the wrong type is a type error before you run anything. The cost is readability: the contract is now buried in constructor calls instead of clean SDL, and a newcomer has to read code, not a document, to understand the API.
The trade-offs, side by side
Section titled “The trade-offs, side by side”flowchart TD SF["Schema-first: write SDL"] SFR["Attach resolvers by name"] CF["Code-first: build types in code"] CFR["Generate SDL from types"] Schema["Executable schema (identical)"] SF --> SFR SFR --> Schema CF --> CFR CFR --> Schema
- Source of truth. Schema-first: the SDL. Code-first: the code.
- Readability of the contract. Schema-first wins — the SDL is plain and shareable. Code-first hides it behind constructors.
- Type safety out of the box. Code-first wins — code and types are checked together. Schema-first needs code generation (next lesson) to catch resolver drift.
- Tooling and collaboration. Schema-first SDL files diff cleanly in review and can be shared with non-engineers and design tools. Code-first changes read like ordinary code changes.
Neither is universally right. Many modern TypeScript stacks land in the middle: write SDL (schema-first) for its readable contract, then generate types from it so you also get the code-first safety. That hybrid is what the rest of this module builds on.
Same schema, two styles, run for real
Section titled “Same schema, two styles, run for real”The demo below builds the identical hello(name) schema twice — once schema-first with buildSchema from SDL, once code-first by constructing types — and prints both schemas as SDL so you can confirm they are the same. Then it runs the same query through each. Press Run.
Notice that printSchema returns the same SDL for both, and both queries return the same data. The destination — a typed, runnable schema — is identical. All that differs is which artifact you hand-write and which one the machine derives. That is the whole essence of the schema-first versus code-first decision.