Getting Started
Basic Query
GraphQL lets clients specify exactly what data they need. A query fetches data, and the response mirrors the query shape. This avoids over-fetching and under-fetching common in REST APIs.
# Query - read data
query {
user(id: 1) {
id
name
email
posts {
title
content
}
}
}
# Response
{
"data": {
"user": {
"id": 1,
"name": "Alice",
"email": "[email protected]",
"posts": [
{ "title": "Hello", "content": "World" }
]
}
}
}Query with Variables
Variables parameterize queries so the same operation can be reused with different inputs. They are declared in the operation header with $name: Type. The query string stays static, only the variables JSON changes per call.
# Declare variables with operation name
query GetUser($id: ID!, $withPosts: Boolean!) {
user(id: $id) {
name
posts @include(if: $withPosts) {
title
}
}
}
# Variables sent alongside the query
{
"id": "1",
"withPosts": true
}Mutation Basics
Mutations modify data and return a selection set just like queries. By convention they should be followed by a read of the changed data. GraphQL executes mutations serially (one after another), while query fields run in parallel.
# Mutation - write data, then return the result
mutation {
createUser(input: { name: "Bob", email: "[email protected]" }) {
id
name
}
}
# Response
{
"data": {
"createUser": {
"id": "42",
"name": "Bob"
}
}
}Fields and Selection Sets
A selection set is the list of fields inside curly braces. Scalars and enums return leaf values; object types require a nested selection set. Forgetting a sub-selection on an object type is a common validation error.
# You must select fields - GraphQL never returns whole objects
query {
user(id: 1) {
# this block is a "selection set"
name
email
}
}
# Selecting a scalar returns its value directly
# Selecting an object REQUIRES a sub-selection set
query {
user(id: 1) {
name # scalar - OK
posts { # object - needs { ... }
title
}
}
}GraphQL vs REST
REST exposes many endpoints with fixed shapes; GraphQL exposes one endpoint where the client defines the shape. This cuts round trips and prevents over/under-fetching. The trade-off is a more complex server and caching via POST.
# REST: multiple round trips, fixed shapes
GET /users/1 -> { id, name, email, address, ... }
GET /users/1/posts -> [ { id, title, body, author, ... } ]
# GraphQL: one request, client-defined shape
query {
user(id: 1) {
name
posts { title }
}
}
# One endpoint, one POST request
POST /graphql
{ "query": "...", "variables": { ... } }Introspection
Introspection lets clients discover the schema at runtime. Tools like GraphiQL, Apollo Studio, and code generators rely on it. The root fields __schema and __type are the entry points. Production servers may disable introspection to hide internals.
# Ask the schema what types and fields it has
{
__schema {
types {
name
kind
}
}
}
# Inspect a specific type's fields
{
__type(name: "User") {
name
fields {
name
type { name kind ofType { name kind } }
}
}
}Schema & Type System
Schema Declaration
The schema block wires root operation types. If your root types are named Query, Mutation, and Subscription, Apollo/GraphQL.js infers the schema automatically and you can omit this declaration entirely.
# SDL - Schema Definition Language
schema {
query: Query
mutation: Mutation
subscription: Subscription
}
# In most servers the schema is inferred from the
# Query/Mutation/Subscription types, so this block is optional.Root Types
Query, Mutation, and Subscription are the entry points of a schema. Query is the only required root type. Each root field becomes an operation clients can run. Naming them as verbs (createUser) for mutations and nouns for queries is conventional.
type Query {
user(id: ID!): User
users(limit: Int = 10): [User!]!
}
type Mutation {
createUser(input: CreateUserInput!): User!
}
type Subscription {
postAdded: Post!
}Type Definitions
Types are defined with the type keyword followed by fields. Each field has a name and a return type. The ! suffix means non-null; [Type!] means a list of non-null items; [Type!]! means a non-null list of non-null items.
# Object type - the most common building block
type User {
id: ID!
name: String!
age: Int
email: String
posts: [Post!]!
}
type Post {
id: ID!
title: String!
author: User!
}Adding Descriptions
Triple-quoted strings (or single-line strings before a definition) become part of the schema as descriptions. They show up in GraphiQL docs and introspection. Plain # comments are dev-only and are stripped out, so use triple-quoted strings for user-facing docs.
"""A registered user of the platform."""
type User {
"""The unique, immutable identifier."""
id: ID!
"Display name shown publicly."
name: String!
# plain # comments are NOT exposed to clients
email: String
}Schema with Extensions
extend type adds fields to a type declared elsewhere. This is how you split a large schema across files or add module-specific fields. A type may be declared once and extended many times, but each field can only be defined once.
# Extend an existing type without redefining it
type User {
id: ID!
name: String!
}
extend type User {
age: Int
posts: [Post!]!
}
# Useful for splitting a schema across files,
# or for adding fields only in certain modules.Schema-First vs Code-First
Schema-first writes SDL by hand and attaches resolvers (Apollo's classic approach). Code-first builds the schema programmatically with TypeScript decorators/builders (Nexus, TypeGraphQL, Pothos), gaining type safety between schema and resolvers. Both produce the same runtime schema.
# 1) Schema-first: write SDL, bind resolvers
const typeDefs = `
type Query { hello: String }
`;
const resolvers = { Query: { hello: () => "world" } };
const server = new ApolloServer({ typeDefs, resolvers });
# 2) Code-first: build with builders (Nexus, TypeGraphQL)
export const Query = queryType({
definition(t) {
t.string("hello", { resolve: () => "world" });
},
});Scalar Types
Built-in Scalars
GraphQL ships five built-in scalars: Int, Float, String, Boolean, and ID. ID is serialized as a string but semantically means an identifier - clients should treat it as opaque. There is no built-in Date or DateTime; you define those yourself.
type Example {
id: ID! # unique identifier, serialized as string
name: String! # UTF-8 text
age: Int # 32-bit integer
score: Float # double-precision float
active: Boolean! # true / false
}Custom Scalars
Custom scalars let you represent domain primitives. After declaring them, you must provide a resolver that serializes (to JSON), parses (from variables), and optionally literal-parses (from inline AST). Otherwise the value is passed through as-is.
# Declare a custom scalar in SDL
scalar DateTime
scalar URL
scalar JSON
type Event {
id: ID!
at: DateTime!
link: URL
metadata: JSON
}Scalar Resolvers
A scalar resolver has three optional methods. serialize converts an internal value to the JSON output. parseValue converts a variable input to internal form. parseLiteral converts an inline literal in the query AST to internal form. Define all three for a fully typed scalar.
const resolvers = {
DateTime: {
// value -> JSON sent to client
serialize: (value: Date) => value.toISOString(),
// variable value -> internal value
parseValue: (value: string) => new Date(value),
// inline AST literal -> internal value
parseLiteral: (ast) => {
if (ast.kind === "StringValue") {
return new Date(ast.value);
}
return null;
},
},
};Date/DateTime Scalar
The graphql-scalars package ships battle-tested scalars: DateTime, Date, Time, EmailAddress, URL, UUID, BigInt, JSON, and more. Prefer it over hand-rolling, especially for ISO 8601 date parsing where edge cases abound.
# Use graphql-scalars for well-tested common types
import { DateTimeResolver } from "graphql-scalars";
const resolvers = {
DateTime: DateTimeResolver,
Date: DateResolver,
Time: TimeResolver,
};
# In SDL
scalar DateTime
type Query {
now: DateTime!
}JSON Scalar
JSON scalars let arbitrary data pass through, useful for dynamic config or metadata. The trade-off: you lose type safety and field-level validation, and clients can't select sub-fields. Use sparingly - a typed object type is almost always better.
scalar JSON
type Query {
# accept arbitrary JSON as input or output
config: JSON
setConfig(value: JSON): JSON
}
# The simplest pass-through resolver
const resolvers = {
JSON: {
serialize: (v) => v,
parseValue: (v) => v,
parseLiteral: (ast) => parseJsonLiteral(ast),
},
};Object Types
Defining Object Types
An object type is a collection of named fields with return types. Object types form the graph - they reference each other to build relationships (User has Posts, Post has Author). Object types are output types; use input types for inputs.
type User {
id: ID!
name: String!
email: String
createdAt: String!
}
type Post {
id: ID!
title: String!
body: String!
published: Boolean!
}Fields and Field Types
Every field declares a return type. Nullable is the default; ! makes it non-null. Lists wrap with []. The position of ! matters: [String] can be null and hold nulls; [String!] can be null but items can't; [String!]! can never be null nor hold nulls.
type Product {
id: ID! # non-null ID
name: String! # non-null string
price: Float! # non-null float
discount: Float # nullable float
tags: [String!]! # non-null list of non-null strings
reviews: [Review] # nullable list of nullable reviews
}Non-Null and Lists
The ! placement controls nullability at each level. Non-null is powerful but risky: if a resolver returns null on a non-null field, the error bubbles up and nulls the parent field too. Default to nullable and opt into non-null where a value is truly guaranteed.
type Example {
a: [String] # may be null; items may be null
b: [String!] # may be null; items NOT null
c: [String]! # NOT null; items may be null
d: [String!]! # NOT null; items NOT null
name: String # may be null
name2: String! # NOT null - errors if null returned
}Nested Objects
Object types reference each other to form the graph, enabling queries that traverse relationships (user -> posts -> author -> profile). Circular references between types are fine and common. The graph is what makes GraphQL a graph, not a tree of endpoints.
type User {
id: ID!
name: String!
profile: Profile!
posts: [Post!]!
}
type Profile {
bio: String
avatar: String
}
type Post {
id: ID!
title: String!
author: User! # back-reference creates the graph
}Field Arguments on Objects
Any field - not just root fields - can take arguments. This lets a single type expose filtered/paginated views of related data (e.g. user.posts(limit: 10)). Arguments with default values become optional in queries.
type User {
id: ID!
name: String!
# an object field can take arguments
posts(limit: Int = 5, after: ID): [Post!]!
role(type: RoleType!): Role
}
type Query {
user(id: ID!): User
}Queries
Query Type
The Query type is the entry point for all reads. Every field here is a top-level operation clients can request. Keep it focused on reads; put writes in Mutation. Naming fields as nouns (users, user, search) is conventional.
type Query {
me: User!
user(id: ID!): User
users(limit: Int = 20, offset: Int = 0): [User!]!
search(term: String!): [SearchResult!]!
}
# Clients run these as operations:
# query { me { name } }
# query { user(id: "1") { name } }Multiple Fields
A query can select multiple root fields in one request. GraphQL executes them in parallel (except in mutations). This batches related fetches into a single round trip, a key efficiency win over REST.
# Several fields in one request - executed in parallel
query {
me { name email }
user(id: "1") { name }
users(limit: 3) { id name }
}
# Response contains all three under "data"
{
"data": {
"me": { "name": "Alice", "email": "[email protected]" },
"user": { "name": "Bob" },
"users": [ { "id": "1", "name": "Bob" }, ... ]
}
}Aliases
Aliases rename fields in the response. They are required when selecting the same field with different arguments - JSON object keys must be unique. Aliases are also handy for shorter or more meaningful response keys.
# Same field twice with different args needs aliases
query {
alice: user(id: "1") { name }
bob: user(id: "2") { name }
}
# Response keys match the aliases
{
"data": {
"alice": { "name": "Alice" },
"bob": { "name": "Bob" }
}
}Nested Queries
Traversing relationships is GraphQL's superpower. The client walks the graph (user -> posts -> comments -> author) in a single query. The server resolves each field via its resolver; deep nesting is fine but watch for N+1 queries on the backend.
query {
user(id: "1") {
name
posts { # traverse relationship
title
comments { # and another level
author {
name
}
}
}
}
}Query with Arguments
Arguments can be inline literals or variables. Inline literals are fine for static values; use variables whenever a value comes from user input or changes per call. Variables keep the query cacheable and avoid string concatenation.
# Schema
type Query {
user(id: ID!, includeEmail: Boolean = false): User
}
# Inline arguments
query {
user(id: "1", includeEmail: true) {
name
email
}
}
# Prefer variables for dynamic values
query($uid: ID!, $withEmail: Boolean!) {
user(id: $uid, includeEmail: $withEmail) {
name
email
}
}Operation Name
Naming operations (query GetUserProfile) is optional but strongly recommended. Named operations are easier to debug, show up in logs and Apollo Studio, and are required when sending multiple operations in one document or for persisted queries.
# Give operations a name - required in production
query GetUserProfile($id: ID!) {
user(id: $id) {
id
name
email
}
}
mutation CreatePost($input: CreatePostInput!) {
createPost(input: $input) {
id
}
}Mutations
Mutation Type
The Mutation type is the entry point for writes. By convention name mutation fields as verbs (createUser, updatePost). Mutations run serially in the order written, so order-dependent side effects are safe across multiple mutation fields in one request.
type Mutation {
createUser(input: CreateUserInput!): User!
updateUser(id: ID!, input: UpdateUserInput!): User!
deleteUser(id: ID!): Boolean!
createPost(input: CreatePostInput!): Post!
}
# Client mutation
mutation {
createUser(input: { name: "Alice", email: "[email protected]" }) {
id
name
}
}Mutation with Input
Use input types for mutation arguments. They group related fields, make the signature clean, and evolve safely (add optional fields without breaking). Always pass mutation payloads through variables, never inline literals.
input CreateUserInput {
name: String!
email: String!
age: Int
}
type Mutation {
createUser(input: CreateUserInput!): User!
}
mutation($input: CreateUserInput!) {
createUser(input: $input) {
id
name
}
}
# { "input": { "name": "Bob", "email": "[email protected]" } }Returning Payloads
A common pattern is a payload object wrapping the result with errors, a success flag, or a client mutation ID. This is more flexible than throwing - you can return partial success and structured validation errors alongside the entity.
# Return the changed entity AND related fields like errors
type CreateUserPayload {
user: User
errors: [UserError!]!
}
type UserError {
field: String
message: String!
}
type Mutation {
createUser(input: CreateUserInput!): CreateUserPayload!
}Multiple Mutations
Unlike queries (parallel), mutations in one operation execute serially in the order declared. This guarantees the second mutation sees the first's effects. If one mutation field errors, later ones still run - validate inputs and use transactions where needed.
# Multiple mutations run SERIALLY in declared order
mutation {
createPost(input: { title: "A" }) {
id
}
publishPost(id: "1") {
publishedAt
}
}Idempotency
Network retries can duplicate writes. An idempotency key lets the server recognize a retry and return the original result instead of creating a duplicate. This is essential for payments, orders, and any non-reversible action.
# Use a client mutation id or idempotency key
input CreateOrderInput {
idempotencyKey: String!
productId: ID!
quantity: Int!
}
type Mutation {
createOrder(input: CreateOrderInput!): Order!
}
# Clients retry with the SAME idempotencyKey on failure
# Server returns the original order instead of creating a duplicateSubscriptions
Subscription Type
Subscriptions push data from server to client when events occur, typically over WebSocket. They are long-lived operations. Keep payloads small - clients usually fetch detailed data with a follow-up query rather than streaming large objects.
type Subscription {
postAdded: Post!
userJoined(roomId: ID!): User!
messageReceived(userId: ID!): Message!
}
# Client subscribes (usually over WebSocket)
subscription {
postAdded {
id
title
author { name }
}
}Subscribe to Events
A subscription resolver exposes a subscribe function returning an AsyncIterator. Mutations (or any server code) publish events to the same topic via pubsub. The optional resolve function transforms the published payload into the field's shape.
# Server-side resolver with asyncIterator
const resolvers = {
Subscription: {
postAdded: {
subscribe: () => pubsub.asyncIterator(["POST_ADDED"]),
resolve: (payload) => payload.postAdded,
},
},
};
# Trigger from a mutation
const resolvers = {
Mutation: {
createPost: async (_, args, { pubsub }) => {
const post = await createPost(args);
await pubsub.publish("POST_ADDED", { postAdded: post });
return post;
},
},
};PubSub
graphql-subscriptions' PubSub works for a single server instance. For multiple server instances behind a load balancer you need a shared broker - redis-pubsub, MQTT, or a Kafka-backed pubsub - so events reach clients connected to any instance.
import { PubSub } from "graphql-subscriptions";
const pubsub = new PubSub();
// publish an event
pubsub.publish("POST_ADDED", { postAdded: post });
// consume in a subscription resolver
subscribe: () => pubsub.asyncIterator(["POST_ADDED"])
# For multi-instance production, swap PubSub for
# Redis, NATS, or Kafka-based implementations.Subscription with Filters
withFilter lets each connected client receive only relevant events - e.g. messages addressed to them. The filter function receives the published payload and the subscription variables. Keep filters cheap; they run for every published event per connected client.
const resolvers = {
Subscription: {
messageReceived: {
subscribe: withFilter(
() => pubsub.asyncIterator(["MESSAGE_RECEIVED"]),
(payload, variables) =>
payload.messageReceived.recipientId === variables.userId
),
resolve: (payload) => payload.messageReceived,
},
},
};
# Only events where the filter returns true reach this clientWebSocket Transport
Subscriptions use the graphql-ws (or legacy subscriptions-transport-ws) protocol over WebSocket. The newer graphql-ws protocol is recommended. connectionParams carry auth tokens. The server must install the WS handler alongside the HTTP endpoint.
# Client uses graphql-ws over WebSocket
import { createClient } from "graphql-ws";
const wsClient = createClient({
url: "ws://localhost:4000/graphql",
connectionParams: { authToken: "..." },
});
wsClient.subscribe(
{ query: "subscription { postAdded { id title } }" },
{
next: (data) => console.log(data),
error: (err) => console.error(err),
complete: () => console.log("done"),
}
);Arguments
Field Arguments
Arguments are named, never positional. They can appear on any field, not just root fields. Named args make queries self-documenting and order-independent. Use variables instead of inline literals when values are dynamic.
type Query {
user(id: ID!): User
posts(limit: Int, offset: Int): [Post!]!
}
# Arguments can be inline literals
query {
user(id: "1") { name }
posts(limit: 10, offset: 0) { title }
}Default Values
Arguments with default values become optional - clients can omit them. Default values are evaluated per request. For non-null arguments there's no default; the client must supply a value. Defaults keep queries terse while staying explicit when needed.
type Query {
# default values make arguments optional
posts(limit: Int = 10, sort: String = "newest"): [Post!]!
user(id: ID!, includeDeleted: Boolean = false): User
}
# These are equivalent:
query { posts { title } }
query { posts(limit: 10, sort: "newest") { title } }Argument Types
Arguments can be scalars, enums, lists, or input object types - never regular object types (those are output-only). Input objects are the idiomatic way to pass complex, structured arguments, especially for filters and pagination.
type Query {
# scalar arguments
byId(id: ID!): User
byAge(min: Int!, max: Int!): [User!]!
# enum argument
byStatus(status: UserStatus!): [User!]!
# input object argument
search(filter: UserFilter!): [User!]!
# list argument
byIds(ids: [ID!]!): [User!]!
}Required vs Optional
The ! suffix on an argument makes it required. Omitting a required argument fails validation before the resolver runs, returning a clear error. Prefer required for genuinely needed inputs and optional-with-default for tunable behavior.
type Query {
# required (non-null) - client MUST provide
user(id: ID!): User
# optional (nullable) - client may omit
posts(limit: Int): [Post!]!
# optional with default
recentPosts(limit: Int = 5): [Post!]!
}
# Omitting a required argument is a validation error
# before any resolver runs.List Arguments
List arguments use square brackets. [ID!]! means a non-null list of non-null IDs - the client must pass a list and every element must be non-null. Pass lists via variables rather than inline literals for cleanliness and to avoid escaping issues.
type Query {
usersByIds(ids: [ID!]!): [User!]!
tags(allOf: [String!]): [Post!]!
}
# Inline list literal
query {
usersByIds(ids: ["1", "2", "3"]) { name }
}
# Via variable
query($ids: [ID!]!) {
usersByIds(ids: $ids) { name }
}
# { "ids": ["1", "2", "3"] }Variables
Variable Definition
Variables parameterize operations. Declare them as $name: Type in the operation header, reference them as $name in the body, and send values in a separate JSON variables field. This separates the static query from dynamic data.
# Declare variables in the operation, then use them
query GetUser($id: ID!) {
user(id: $id) {
name
email
}
}
# Variables are sent in a separate JSON object
{
"id": "1"
}Variable Types
Variable types must match the argument types in the schema. Variables can be nullable, non-null, lists, or input objects. A variable's type must be an input type (scalar, enum, or input object) - never an output object type.
query Search(
$term: String!
$limit: Int = 10
$filters: PostFilter
$tags: [String!]
) {
search(term: $term, limit: $limit, filters: $filters, tags: $tags) {
title
}
}Default Values
Variable defaults apply when the client omits the variable. They are only legal on nullable variables - a non-null variable means the client MUST supply a value. Defaults let clients send minimal variable sets while servers stay flexible.
query Posts($limit: Int = 10, $sort: String = "newest") {
posts(limit: $limit, sort: $sort) { title }
}
# If the client omits "limit", it defaults to 10.
# Only nullable variables can have defaults - a non-null
# variable with a default is a contradiction.Required Variables
A non-null variable (!) is required - omitting it fails validation before the resolver runs. This is the safest way to guarantee an input exists. Non-null variables cannot have defaults; if you want a default, make the variable nullable.
query User($id: ID!) { # ! means required
user(id: $id) { name }
}
# Missing required variable -> validation error
{ "id": "1" } # OK
{ } # ERROR: variable $id of type ID! is required
# A non-null variable cannot have a default value.Using Variables with Directives
Variables can drive directives like @include and @skip, letting clients toggle fields dynamically. This is great for feature flags, A/B tests, or conditional sections - all with a single cached query document.
query User($id: ID!, $withPosts: Boolean!) {
user(id: $id) {
name
posts @include(if: $withPosts) {
title
}
}
}
# { "id": "1", "withPosts": false } -> posts omitted
# { "id": "1", "withPosts": true } -> posts includedFragments
Named Fragments
Fragments are reusable selection sets bound to a specific type (fragment Name on Type). Spread them with ...Name wherever that type appears. They keep queries DRY and let clients share field selections across operations.
# Define a reusable fragment
fragment UserFields on User {
id
name
email
}
# Spread it into queries
query {
user(id: "1") {
...UserFields
}
users {
...UserFields
}
}Fragment Composition
Fragments can spread other fragments, building up composite selections. This modularizes field choices (basic vs full profile) and lets UI components each declare their own data needs, then compose them into one query.
fragment UserBasic on User {
id
name
}
fragment UserWithEmail on User {
...UserBasic
email
}
fragment UserFull on User {
...UserWithEmail
posts { title }
}
# Fragments can spread other fragments of the same type.Fragment Variables
Relay-style fragment variables let a fragment accept parameters (like a limit) that callers pass via @arguments. This makes fragments truly reusable with different settings. Apollo supports this via the @arguments directive from its client directives.
fragment UserPosts on User
@argumentDefinitions(limit: { type: "Int", defaultValue: 5 }) {
posts(limit: $limit) {
title
}
}
query {
user(id: "1") {
...UserPosts @arguments(limit: 10)
}
}
# @argumentDefinitions and @arguments come from
# Relay; Apollo uses a similar pattern via client directives.Reusable Fragments
Colocating fragments with the components that render them is a powerful pattern: each component declares its data needs, and the parent composes them into one query. This scales data fetching in large apps (Relay and Apollo both support it).
// Colocate fragments with the UI component that uses them
const USER_CARD = graphql`
fragment UserCard_user on User {
id
name
avatarUrl
}
`;
function UserCard({ user }) {
return <div>{user.name}</div>;
}
// The parent query spreads all needed fragments
query {
users {
...UserCard_user
}
}Fragment Caveats
A fragment declared on Post can only be spread where a Post is expected - spreading it on a User is a validation error. Use inline fragments with type conditions to spread conditionally on unions/interfaces. Also avoid circular fragment spreads.
# A fragment must match the type where it's spread
fragment PostFields on Post { title body }
query {
user(id: "1") {
...PostFields # ERROR: user is not a Post
}
}
# Fragments can only spread on the declared type
# (or via type conditions with inline fragments).Inline Fragments
Inline Fragment Basics
An inline fragment is an unnamed selection set prefixed with ... on Type. It applies fields only when the runtime object matches the type condition. This is how you access type-specific fields on interfaces and unions.
# An inline fragment is a selection set with a type condition
query {
user(id: "1") {
id
name
# fields common to User
... on AdminUser {
permissions
}
}
}
# No name - declared inline in the query.Type Conditions
The ... on Type syntax is a type condition. Multiple inline fragments in one selection let you handle each possible type of a union/interface separately. Fields shared by all types can be selected directly outside the fragments.
query Search($term: String!) {
search(term: $term) {
... on User { name email }
... on Post { title author { name } }
... on Comment { body }
}
}Union Access
Unions have no shared fields beyond __typename, so to read type-specific data you must use inline fragments. __typename is always selectable and tells the client which concrete type each item is - essential for rendering union results.
union SearchResult = User | Post | Comment
query {
search(term: "graphql") {
# shared __typename is always available
__typename
... on User { name }
... on Post { title }
... on Comment { body }
}
}Interface Access
Interfaces declare shared fields. Those shared fields can be selected directly on the interface; type-specific fields need inline fragments. Unlike unions, interfaces guarantee a common set of fields every implementing type must provide.
interface Node { id: ID! }
type User implements Node { id: ID! name: String! }
type Post implements Node { id: ID! title: String! }
query {
node(id: "1") {
id # shared interface field - selectable directly
... on User { name }
... on Post { title }
}
}Named Fragment with Type Condition
Named fragments carry their own type condition, so you can spread them on a union/interface result and each applies only to its matching type. This combines reusability with conditional selection - spread several named fragments to cover all union members.
# Named fragments can also carry a type condition
fragment UserFields on User { name email }
fragment PostFields on Post { title }
query {
search(term: "a") {
...UserFields
...PostFields
}
}
# Equivalent to inlining those fragments inline.Directives
Built-in Directives
GraphQL ships two built-in client directives: @skip(if: Boolean!) omits the field when true, @include(if: Boolean!) includes it only when true. They are mutually exclusive and can be applied to fields, fragment spreads, and inline fragments.
# @skip - omit field if true
query($skip: Boolean!) {
user(id: "1") {
name
email @skip(if: $skip)
}
}
# @include - include field only if true
query($withPosts: Boolean!) {
user(id: "1") {
posts @include(if: $withPosts) { title }
}
}@skip and @include
@skip and @include let one query serve multiple shapes. They take a Boolean variable, so the same persisted query can render a summary or a detailed view depending on variables - no need to maintain two query strings.