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

The Query Root Type

How the Query type defines every readable entry point clients can call.

35m
1 problems
Topic Progress 0%

The Schema Must Have a Door

The Schema Must Have a Door

You can define fifty object types and still ship an API nobody can call. A schema needs an entry point: a type whose fields become the legal first words of every operation. The specification is blunt about it.

The query root operation type must be provided and must be an Object type.

Two details hide in that sentence. First, the query root is required while the mutation and subscription roots are optional; a service that only reads still needs type Query, but a service with no writes may omit Mutation entirely. Second, each root that is present must be an Object type, and all three roots must be distinct types - the schema cannot use one object as both query and mutation root, because their execution rules differ.

type Query {
  product(id: ID!): Product
  me: User
}

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

That three-line schema is complete and executable. Everything else in the file exists to be reachable from these fields: Product is queryable because Query.product returns it, and a type no root can ever reach is dead weight that only introspection will show.

The reverse case catches people out. If a document contains a type named Query and no schema block, SDL presumes that type is the query root; if it contains a type named Mutation but no schema block, the presumption silently applies there too, which can wire a domain type into the wrong role. Chapter three shows the fix.

One more property of the root: introspection. The meta-fields __schema and __type are available on the query root implicitly and do not appear in its declared field list, which is why a client can always ask the server what it can query, see introspection mechanics.

Designing Top-Level Entry Points

Designing Top-Level Entry Points

The fields of Query are the API surface. Everything a client can start with is on that one type, so its design decides how discoverable and how safe the API is.

type Query {
  product(id: ID!): Product
  products(filter: ProductFilter, first: Int = 20, after: String): [Product!]!
  productsInStock(filter: ProductFilter): [Product!]!
  me: User
  node(id: ID!): Node
}

Read the list as a set of decisions rather than a list of names:

  • Nouns, not actions. product and products are reads; creating or changing things belongs on the mutation root, covered in mutation operations. A createProduct field on Query looks harmless until something caches that GET.
  • Nullability encodes the contract. product(id: ID!) returns Product because a missing id is a normal outcome, while me: User returns a non-null only if your service really cannot represent an anonymous caller. Lists that clients always render are declared [Product!]! so a null item never blanks the screen; see enums, lists, and non-null.
  • Arguments do the filtering. One well-argumented field beats three near-duplicates: filter, pagination arguments, and sorting cover the space that productsInStock and friends would otherwise fork into. Argument shape and pagination conventions live in field arguments design and Relay-style pagination.
  • A generic node(id: ID!) field gives clients one way to fetch any object by id, which is how Relay clients navigate without knowing types up front.

At execution, each requested top-level field invokes its own resolver with an unusual first argument: the parent value for a root field is effectively nothing (the root object), so root resolvers start from args, context, and info rather than from a parent record. After that the mechanics are identical to any other field, see resolver fundamentals.

Interview angle: "How would you expose a search endpoint?" expects the whole design: the field name, its arguments, why the return type is a list of non-null items, and where the pagination cursor comes from.

Renaming the Roots

Renaming the Roots

By default SDL maps the roots by name: a type called Query is the query root, Mutation the mutation root, Subscription the subscription root. When the names do not match, an explicit schema block does the mapping.

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

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

type RootMutation {
  addToCart(sku: String!, qty: Int!): Cart
}

Why would you want non-default names? Two situations come up repeatedly.

Composition. When several schemas are stitched or federated into one graph, each contributor needs its own root to avoid collisions. A schema block per subgraph (or the composition tooling's equivalent) keeps one team's Query from overwriting another's; see schema composition guidelines and federation architecture.

Domain collisions. If your domain already has a meaningful type called Mutation or Query, the default presumption wires the wrong type into the root role. The specification's own example makes this explicit: a schema containing a type named Mutation but no schema block would treat it as the mutation root, so the block is required to say "no, that type is just data."

Three rules bound what a schema block may contain:

  1. At most one schema definition per document.
  2. Every root it names must be an Object type.
  3. Roots must be distinct from one another.

The rules exist because execution depends on which type is which: query root fields may run in parallel while mutation root fields run serially, and subscription root fields drive event streams. If one type could serve two roles, those guarantees would contradict each other.

Interview angle: "What must every schema have?" Answer: a query root, provided and an Object type; mutation and subscription roots are optional, and if present they must be different types.

Practice Problems

0 / 1 solved
Write the Query Root for an Inventory API
Schema entry points

Design the Query type for an inventory service. Requirements: fetch a single part by id (a part may not exist), list parts with a free-text search term and a maximum result count defaulting to 25, list parts below a given stock threshold, and fetch the signed-in operator. Types Part and Operator already exist. Avoid near-duplicate fields by carrying variation in arguments, and state the nullability of every return type.

Example:

Input: Four read needs: byId, search, low stock, current operator

Output: type Query { part(id: ID!): Part searchParts(term: String!, limit: Int = 25): [Part!]! lowStock(threshold: Int!): [Part!]! me: Operator }

The single-item lookup is nullable because a missing id is normal; lists are [Part!]! so one bad row cannot blank the response; search and threshold are arguments on list fields rather than four separate list fields.

Optimal Solution — O(f) fields validated where f is root size time, O(f) space

Group reads by shape: one nullable lookup for a single record, argument-driven list fields for collections, and a session-dependent field for the caller. Nullability states which outcomes are normal.

type Query {
  part(id: ID!): Part
  searchParts(term: String!, limit: Int = 25, offset: Int = 0): [Part!]!
  lowStock(threshold: Int!): [Part!]!
  me: Operator
}

Edge Cases:

  • part(id) must be nullable: returning null for an unknown id is a normal outcome, not an error
  • An empty search result is an empty list, which satisfies [Part!]!

Quiz

1. What does the specification require of a valid schema?

Question 1 options

2. A schema defines type Query and type Mutation but no schema block. What does SDL assume?

Question 2 options

3. What does a resolver for a top-level Query field receive as its first argument?

Question 3 options

4. Why do teams rename root types with a schema block?

Question 4 options

Flashcards

Question

Required root types

Answer

Query is mandatory and must be an Object type. Mutation and Subscription are optional; if present they must be Objects, and all three roots must be distinct types.

Question

Default root names

Answer

Without a schema block, SDL presumes types named Query, Mutation, and Subscription are the roots. A domain type with one of those names gets wired in by accident.

Question

What a root resolver sees

Answer

parent is undefined at the root; the useful inputs are args, context, and info. Everything below the root behaves like any other field.

Question

How to spot a dead type

Answer

If no root field can reach a type, no operation can select it. Reachability from Query (and Mutation/Subscription) is the test.

Revision Notes

Key Takeaways

  • 1. A schema without a query root is invalid; the root must be an Object type and distinct from the other roots
  • 2. Query fields are the entire read surface: name them as nouns, put filtering in arguments, and let nullability state the contract
  • 3. Root resolvers have no parent to start from, so they read args, context, and info
  • 4. A schema block maps custom root names and prevents domain types from being presumed as roots

Interview Tips

  • • Answer "what must every schema have" completely: a provided query root that is an Object type, plus the rules for optional roots
  • • When asked to design an API, start at the Query type and defend each field name, argument, and return nullability

Cheat Sheet

Query root cheat sheet

schema { query: RootQuery mutation: RootMutation }
type RootQuery {
  product(id: ID!): Product
  products(filter: ProductFilter, first: Int = 20, after: String): [Product!]!
}

Rules

  • query root required, must be Object; mutation/subscription optional
  • all roots must be different types; one schema definition per document
  • default names Query / Mutation / Subscription apply when no schema block exists
  • introspection (__schema, __type) is implicit on the query root

Design: nouns not actions, args for filter/sort/paginate, nullable lookup + [T!]! lists, resolvers get (undefined, args, context, info).

Continue Learning

Before You Start

Next Topics

Related Reading