A Stronger Contract Than JSON
A Stronger Contract Than JSON
A REST handler returns an object and the client finds out what is in it at runtime. If the handler renames totalPrice to amount, every consumer breaks silently until someone reads the payload. GraphQL replaces that guesswork with a type system that lives on the server.
type Product {
id: ID!
title: String!
priceCents: Int!
inStock: Boolean!
}
Each line is a promise. id: ID! says the field always exists and serializes as a string. priceCents: Int! says the value is a signed 32-bit integer, which is narrower than JSON's number and therefore a stronger guarantee than "some number will be there." The exclamation mark is the non-null modifier, covered in detail in enums, lists, and non-null.
The guarantee is enforced, not documented. When a client asks for a field the type does not declare, validation rejects the request before execution:
Validation error: Cannot query field "price" on type "Product".
Did you mean "priceCents"?
No resolver ran, no query reached the database, and the client learns the correct spelling from the error message. In REST the same mistake returns a payload with a missing key that surfaces three screens later as undefined.
Because the contract is structured data, tools can read it. Introspection lets a workbench print every type and field, a linter enforce naming rules, and a generator produce typed client code; see introspection mechanics and TypeScript code generation. The schema stops being a wiki page and becomes the source both sides compile against.
Turning Rows Into a Graph
Turning Rows Into a Graph
Object types are useful alone; they become a graph when they reference each other.
type User {
id: ID!
displayName: String!
orders: [Order!]!
}
type Order {
id: ID!
placedAt: String!
buyer: User!
items: [OrderItem!]!
}
Two structural facts follow, and they are the reason the technology carries the name it does.
References are just fields. User.orders and Order.buyer point at other object types, so a client can walk from a user to their orders and back to the buyer in one operation. REST would answer that with either embedded JSON that duplicates data or a second call the client has to discover and make.
Cycles are legal. User -> Order -> User is a valid schema, and so is a self-reference such as User.friends: [User!]. The type system describes shape, not traversal; preventing a client from looping forever is a separate concern handled by query depth limits rather than by forbidding the cycle in the schema.
A few rules keep the graph coherent:
- Field names must be unique within a type; two
titlefields on one type are a schema error, not an overload. - Object-typed fields must declare their sub-selection in queries, while scalar and enum fields must not, so the shape of every request is unambiguous.
- Arguments attach to fields, which is how a single field like
orders(status: OrderStatus)serves many queries; see field arguments design for how to shape them. - Names beginning with a double underscore are reserved for introspection (
__typename,__schema,__type). Declaring__internalIdin your own schema collides with machinery the platform owns, so tools reject it.
The design question you will actually face is not syntax but granularity: which relationships become fields, and which stay behind an argument. Getting that wrong produces a schema where every query triggers a full traversal, and that is a performance decision dressed as a typing decision.
Validation Runs Before Any Resolver
Validation Runs Before Any Resolver
The pipeline behind a GraphQL request is parse, validate, execute. Object types are what make the middle step powerful, so it pays to know exactly what validation checks and what it deliberately does not.
What validation guarantees:
| Check | Failure you see |
|---|---|
| Field exists on the type | Cannot query field "prcie" on type "Product" |
| Sub-selection present on object fields | Field "orders" of type "[Order!]!" must have a selection of subfields |
| No sub-selection on leaf fields | Field "title" must not have a selection of subfields |
| Arguments exist and match types | Unknown argument "staust" on field "orders" |
| Same response key not selected twice with conflicting shapes | Fields conflict because they return differing types |
What validation cannot check: whether your resolvers return data of the declared type. The schema describes intent; the resolver is where intent meets the database. A priceCents: Int! resolver that returns a string produces an execution-time field error rather than a validation error, which is why the two categories must be kept separate when you debug.
Practical consequences for a team:
- Reviewing a schema pull request reviews the API surface itself, because that text is what clients compile against.
- Broken client queries fail in CI or in the workbench, not in production.
- Empty selection sets are rejected: a query must ask for at least one field, so
query { product(id: "1") { } }never reaches execution.
Interview angle: "a client requests a field that does not exist; what happens and how do you know?" Answer with the phase (validation), the absence of side effects (no resolver, no database call), and the tool that surfaces it (the same validation rules that run in CI and the workbench).
Practice Problems
Model a support desk as GraphQL object types. Requirements: a Ticket has an id, subject, status drawn from a fixed set (OPEN, PENDING, CLOSED), and a creator; a Customer has an id, email, and their tickets; an Agent has an id, name, and the tickets they handle; tickets carry a list of comments where each comment has body and author. Every list that a screen always renders must be non-null, and every id must be an ID. Deliver the SDL.
Example:
Input: Requirements: Ticket/Customer/Agent/Comment with two people referencing tickets and tickets carrying comments
Output: type Ticket { id: ID! status: OrderStatusLikeEnum! creator: Customer! assignee: Agent comments: [Comment!]! } plus the other three types
Each relationship becomes a field pointing at another object type, so a single query can walk ticket -> creator -> tickets without extra round trips.
Optimal Solution — O(n) field validations where n is schema size time, O(n) space
Start from the nouns, give each an id, then express each relationship as a field whose type is the referenced object (or a list of it), and use non-null on anything a screen cannot render without.
enum TicketStatus {
OPEN
PENDING
CLOSED
}
type Comment {
id: ID!
body: String!
author: Customer!
}
type Customer {
id: ID!
email: String!
tickets: [Ticket!]!
}
type Agent {
id: ID!
name: String!
assignedTickets: [Ticket!]!
}
type Ticket {
id: ID!
subject: String!
status: TicketStatus!
creator: Customer!
assignee: Agent
comments: [Comment!]!
} Edge Cases:
- An unassigned ticket needs a nullable assignee; forcing Ticket.assignee: Agent! would make unassigned tickets unrepresentable
- An empty comments list is [] and satisfies [Comment!]!; null does not
Quiz
1. A client queries product { prcie } and the schema declares priceCents: Int!. What is the outcome?
2. Why is User.orders: [Order!]! with Order.buyer: User! a legal schema?
3. What does the double-underscore prefix mean in a GraphQL schema?
4. Which statement about object-typed fields is correct?
Flashcards
Question
Object type in SDL
Click to reveal answer
Answer
type Name { field: Type, other(arg: ArgType): Type } - declares named fields, each with a name and a return type, validated on the server.
Question
What validation guarantees vs execution
Click to reveal answer
Answer
Validation proves the document matches the schema (fields, arguments, selections). It cannot prove resolvers return the declared types; those failures appear during execution.
Question
Relationships in the schema
Click to reveal answer
Answer
Object types reference each other as fields (User.orders: [Order!]!, Order.buyer: User!). Cycles are legal; loop protection comes from depth limits.
Question
Reserved name prefix
Click to reveal answer
Answer
Names starting with __ are reserved for introspection (__typename, __schema, __type) and must not be declared in user schemas.
Revision Notes
Key Takeaways
- 1. A field declaration is a runtime guarantee enforced by validation, not documentation
- 2. Object types reference each other, and those references are what make the API a graph rather than a set of endpoints
- 3. Validation runs before execution, so schema mistakes never reach resolvers or databases
- 4. Field names are unique per type and the __ prefix belongs to introspection
Interview Tips
- • Answer "what stops a client from querying a nonexistent field?" with the phase, the absence of side effects, and the tool that surfaces the error
- • Be ready to draw a small domain as object types with references and defend each relationship choice
Cheat Sheet
Object types and fields
type Product {
id: ID!
title: String!
priceCents: Int!
reviews(first: Int): [Review!]!
}
Rules
- Every field has a name and a return type; names unique within a type
- Object field in a query needs a selection set; leaf field must not have one
- References between types create the graph; cycles are legal (guard with depth limits)
- __ prefix reserved for introspection
- Arguments live on fields, not types
Pipeline: parse -> validate (fields, args, selections) -> execute (resolvers, type coercion)