Skip to content
intermediate Phase 2 · Schema Foundations: Types and the Query Root

Schema Definition Language Documents

Writing schemas in SDL with type, extend, and schema blocks and multiple root types.

40m
1 problems
Topic Progress 0%

SDL as a Language-Neutral Contract

SDL as a Language-Neutral Contract

The type system definition language - SDL for short - is how a GraphQL schema is written as text. Its value is that the text means the same thing to every tool and every language, so the same file can drive a Node server, generate a TypeScript client, and be diffed in a pull request.

"""
A purchasable item in the catalog.
"""
type Product {
  id: ID!
  title: String!
  price: Money!
  tags: [String!]!
}

scalar Money

enum StockLevel {
  OUT
  LOW
  HIGH
}

input ProductFilter {
  query: String
  inStockOnly: Boolean = false
  maxPrice: Money
}

type Query {
  product(id: ID!): Product
  products(filter: ProductFilter, limit: Int = 20): [Product!]!
}

Each definition kind has one job: type for output objects, input for values that only travel inward (using type for arguments would let clients read a field that is meant to be written), enum for closed sets, scalar for leaf types, and triple-quoted descriptions as documentation attached to the thing they describe - block strings entered the specification in June 2018 specifically for this purpose, see GraphQL specification history.

One conceptual point decides how you read every schema you meet: SDL describes shape, not behavior. Two entirely different servers can publish identical SDL with completely different resolvers behind it. The document says what a client may ask for; it says nothing about where the data comes from, how it is authorized, or how fast it is, see resolver fundamentals.

That separation is exactly why SDL is the artifact teams review. A schema pull request is reviewable in the same diff as code, a linter can enforce naming conventions before merge, and a generator can emit typed clients from it; see schema linting rules and TypeScript code generation.

Wiring Roots with a schema Block

Wiring Roots with a schema Block

The schema block is the one place SDL describes wiring rather than shape: it says which object type plays which root role.

schema {
  query: RootQuery
  mutation: RootMutation
  subscription: RootSubscription
}

type RootQuery {
  product(id: ID!): Product
}

type RootMutation {
  renameProduct(id: ID!, title: String!): Product
}

Without the block, SDL falls back to names: a type called Query becomes the query root, Mutation the mutation root, Subscription the subscription root. The block exists for the three cases where that presumption is wrong or incomplete:

  1. Custom names. Your roots are called RootQuery and RootMutation, so the mapping must be explicit.
  2. Partial roots. A read-only service has a query root and no mutation root at all. If a type named Mutation happens to exist as domain data, the default presumption would wire it into the root role, and only a schema block saying query: Query prevents that - this exact trap appears in the specification's own examples.
  3. Composition. When subgraphs are composed into a larger graph, each schema names its own roots so one contributor's Query does not overwrite another's, see federation architecture.

The block itself is constrained: at most one schema definition per document, every root it names must be an Object type, and the roots must be three distinct types. Subscriptions are the rarest of the three and only exist if the service streams events, see subscription lifecycle.

A practical reading habit: when you open an unfamiliar schema, find the schema block (or infer the defaults) before reading any type. Knowing where execution starts turns a pile of types into an API you can navigate, and it is the first thing a workbench does when it renders the explorer, see GraphiQL workbench.

Extensions, Printing, and Diffing

Extensions, Printing, and Diffing

A schema owned by one team fits in one file. A schema owned by ten teams does not, and extend is what keeps it coherent.

# cart/schema.graphql
type Query {
  cart(id: ID!): Cart
}

# recommendations/schema.graphql
extend type Query {
  recommendations(limit: Int = 3): [Product!]!
}

extend type Product {
  recommendationReason: String
}

Extensions add fields to types declared elsewhere, which lets each team ship its own file and lets the build assemble them. The constraint that matters: an extend needs a base definition somewhere in the document, so a module that extends Product cannot load on a server that never defined Product. Ordering is handled by the loader, but the dependency is real, see schema composition guidelines.

For everything after assembly, the printed form is the unit of work:

import { printSchema, lexicographicSortSchema } from 'graphql';

// normalized text: field order sorted, so diffs show real changes
const sdl = lexicographicSortSchema(schema);
fs.writeFileSync('schema.graphql', printSchema(sdl));

Sorting first is not cosmetic: without it, a field appended to a type shifts the diff even when nothing else changed. Once SDL is a stable text artifact, the rest of the toolchain plugs in:

  • Schema checks in CI diff the proposed print against the current one and block breaking changes such as removed fields or narrowed nullability, see schema checks in CI.
  • Documentation sites render the same text with descriptions attached.
  • Consumers generate typed clients from it, and gates protect them from silent drift.
  • Federation publishes subgraph SDLs to the router, which composes them into a supergraph, see subgraph composition.

Interview angle: "How do you review a schema change?" Start with the printed, sorted SDL diff, classify each hunk as additive or breaking, and name the CI check that enforces the classification.

Practice Problems

0 / 1 solved
Assemble a Split Schema into One Valid Document
Schema composition

Three teams ship three SDL fragments for the same service. Fragment A: type Query { product(id: ID!): Product } and type Product { id: ID! title: String! }. Fragment B: extend type Query { searchProducts(term: String!): [Product!]! }. Fragment C: type Mutation { renameProduct(id: ID!, title: String!): Product } - but the team wants the root type called RootMutation. Assemble the fragments into one complete schema document with an explicit schema block, and state what would break if fragment B loaded on a server that never saw fragment A.

Example:

Input: Fragments A, B, C with custom mutation root name RootMutation

Output: One document: schema { query: Query mutation: RootMutation } + type Query + extend converted to a base field or kept with its base present + type RootMutation containing renameProduct

The schema block is mandatory because RootMutation is not a default name; the extension is harmless only because Product and Query are defined in the same assembled document.

Optimal Solution — O(t + e) where t is types and e is extensions time, O(t + e) space

Gather base definitions first, fold extensions into their bases (or keep them as extend statements with bases present), rename the mutation root, and add one schema block that maps both roots explicitly.

schema {
  query: Query
  mutation: RootMutation
}

type Query {
  product(id: ID!): Product
  searchProducts(term: String!): [Product!]!
}

type Product {
  id: ID!
  title: String!
}

type RootMutation {
  renameProduct(id: ID!, title: String!): Product
}

Edge Cases:

  • Fragment B without Fragment A fails assembly: extend has no base type to attach to
  • A type named Mutation left in the document without a schema block would be presumed as the mutation root, defeating the rename

Quiz

1. What does the extend keyword do in SDL?

Question 1 options

2. A schema has a query root named RootQuery. What must the document contain?

Question 2 options

3. Why sort the schema before printing it for version control?

Question 3 options

4. Which statement about SDL is correct?

Question 4 options

Flashcards

Question

SDL definition kinds

Answer

schema (root wiring), type (output objects), input (inbound values), enum (closed set), scalar (leaf), interface/union (abstraction), directive (extension points), extend (add fields).

Question

When a schema block is required

Answer

Root names differ from Query/Mutation/Subscription, or a domain type shares one of those names and must not be wired as a root. At most one schema definition per document.

Question

extend semantics

Answer

Adds fields to a type declared elsewhere; the base definition must exist somewhere in the assembled document, which is what makes multi-file schemas work.

Question

printSchema workflow

Answer

lexicographicSortSchema then printSchema produces canonical SDL text for docs, codegen, CI diffs, and federation publication.

Revision Notes

Key Takeaways

  • 1. SDL is implementation-neutral text, which is why it can be reviewed, linted, diffed, and used to generate clients
  • 2. The schema block wires roots; defaults apply only to the names Query, Mutation, and Subscription
  • 3. extend lets a schema live across many files as long as every base type is defined somewhere
  • 4. Printed, sorted SDL is the artifact CI compares to keep schema changes safe

Interview Tips

  • • Describe your schema review process: normalized print, additive-versus-breaking classification, and the CI gate that enforces it
  • • Explain the difference between what SDL guarantees (shape) and what it cannot (resolver behavior, authorization, performance)

Cheat Sheet

SDL cheat sheet

schema { query: RootQuery mutation: RootMutation }

scalar Money
enum StockLevel { OUT LOW HIGH }
input ProductFilter { query: String inStockOnly: Boolean = false }
type Product { id: ID! price: Money! }
extend type Product { recommendationReason: String }
type RootQuery { product(id: ID!): Product }

Rules

  • defaults only for Query / Mutation / Subscription names
  • one schema definition per document; roots must be Objects and distinct
  • extend needs its base type defined somewhere in the assembled document
  • descriptions use triple-quoted block strings

Workflow: lexicographicSortSchema -> printSchema -> commit -> CI diffs proposed vs current -> codegen and docs from the same text.

Continue Learning