How it works
The explanation, with examples you can run.
A small schema
"""A person who writes posts."""type User { id: ID! name: String! email: String # nullable: hidden from other users posts(first: Int = 10, after: String): PostConnection!} type Post implements Node { id: ID! title: String! author: User! tags: [String!]! publishedAt: DateTime} interface Node { id: ID! } union SearchResult = User | Post scalar DateTime @specifiedBy(url: "https://www.rfc-editor.org/rfc/rfc3339") enum Role { READER WRITER ADMIN } input CreatePostInput { title: String! body: String! tags: [String!] = []} type Query { me: User node(id: ID!): Node search(text: String!): [SearchResult!]!} type Mutation { createPost(input: CreatePostInput!): Post!}The building blocks:
- Scalars:
Int,Float,String,Boolean,ID, plus custom ones likeDateTime, whose format@specifiedBycan document. - Object types have fields; input objects are their counterpart for arguments. The two never mix: arguments take only scalars, enums and input objects.
- Enums are a closed set of values.
- Interfaces declare fields that implementing types must have; unions list possible types without shared fields.
- Lists
[T]and non-nullT!wrap other types and combine. - The September 2025 edition adds
@oneOfinput objects, where exactly one field must be set: a typed way to say "find a user by id or by email, but not both".
Nullability is a design decision
Every field is nullable unless you add !. That is deliberate. If a resolver fails or a backend times out, a nullable field can come back as null with an entry in the response's errors, and the rest of the result still arrives. A non-null field promises a value; if it can't keep that promise, the null moves up to the nearest nullable parent, possibly wiping out much more of the response. A common rule of thumb is to make identifiers and fields that are always present non-null, and leave fields backed by other services nullable.
The schema is also data
A running server can describe itself through introspection: the __schema and __type fields that any query can ask for, and __typename on every object. Explorers and code generators use it. Whether to allow it in production is a security question covered later in this path.
Check yourself
3 questions. Get them all right to finish this topic.
0 of 3 right so far. Get all three to finish this topic.