Call
APIs

What Is GraphQL? Schema, Queries, Mutations, Resolvers, and the Type System Explained

GraphQL is a typed query language and execution model for APIs. Learn how schemas, object types, fields, arguments, variables, fragments, mutations, nullability, resolvers, introspection, and GraphQL-over-HTTP fit together.

Published: May 22, 2025
Updated: September 12, 2026
13 min read
What Is GraphQL? Schema, Queries, Mutations, Resolvers, and the Type System Explained

What Is GraphQL? Schema, Queries, Mutations, Resolvers, and the Type System Explained

GraphQL is a typed query language and execution model for APIs. A GraphQL service publishes a schema describing what clients can ask for, clients send queries or mutations against that schema, and the server executes those operations using application code and data sources behind the API.

The defining idea is simple:

the client describes the response shape it needs, and the server validates that request against a known type system.

GraphQL does not require a particular database, programming language, or backend architecture. It can sit in front of PostgreSQL, REST services, gRPC services, caches, files, or several systems at once.

The current published GraphQL specification edition is September 2025.

Start with a schema

A GraphQL API begins with a schema.

For example:

type Product {
  id: ID!
  name: String!
  price: Float!
  seller: Seller!
}

type Seller {
  id: ID!
  name: String!
}

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

This schema tells clients:

  • Product exists
  • which fields a product exposes
  • the type of each field
  • which fields are nullable
  • which arguments a query accepts
  • which root operations are available

The schema is both an API contract and the foundation for validation/tooling.

Object types describe available fields

GraphQL object types represent structured API values.

type User {
  id: ID!
  name: String!
  email: String
}

Fields can return:

  • built-in scalar values
  • custom scalars
  • enums
  • other objects
  • interfaces or unions
  • lists

The field declaration says what a client is allowed to request—not necessarily how the backend stores the data.

A User object can be assembled from a SQL row, another service, a cache, or several data sources.

Clients select fields explicitly

A client query names the fields it needs:

query GetProduct {
  product(id: "p42") {
    id
    name
    price
  }
}

A typical result mirrors the requested shape:

{
  "data": {
    "product": {
      "id": "p42",
      "name": "Mechanical Keyboard",
      "price": 4999
    }
  }
}

The server does not automatically return every field in Product.

This client-selected response shape is one of GraphQL's most recognizable properties.

Nested selection follows relationships in the schema

If Product.seller returns a Seller, clients can request nested fields:

query GetProduct {
  product(id: "p42") {
    name
    seller {
      id
      name
    }
  }
}

The query syntax follows the graph of types.

That does not mean the backend is a graph database. “Graph” describes the connected API type system and traversal model.

The underlying data may still be relational tables or remote services.

Scalar fields are leaves

GraphQL includes built-in scalar types such as:

  • Int
  • Float
  • String
  • Boolean
  • ID

Scalars are leaf values, so they do not have sub-selections.

This is valid:

product(id: "p42") {
  name
}

This is not:

product(id: "p42") {
  name {
    something
  }
}

because name is a scalar rather than an object type.

Custom scalars represent domain-specific values

GraphQL servers can define custom scalar types:

scalar DateTime
scalar Money
scalar URL

A custom scalar needs clear serialization and parsing rules.

For example, a DateTime scalar should define whether values use an ISO/RFC-style timestamp format and what timezone behavior clients can expect.

Do not create custom scalars merely to rename ordinary strings. They are most useful when a value has real domain semantics or validation rules.

! means non-null

GraphQL distinguishes nullable and non-null fields.

name: String!
nickname: String

This means:

  • name cannot be null in a valid result
  • nickname may be null

Nullability is part of the API contract and affects error propagation.

If a resolver fails to produce a value for a non-null field, GraphQL propagates null upward until it reaches a nullable boundary, while also reporting an error.

Choose nullability carefully.

Marking every field non-null can make partial failures propagate farther than you intended.

Making everything nullable forces clients to handle ambiguity everywhere.

Lists have two separate nullability decisions

These types are different:

[String]
[String!]
[String]!
[String!]!

They answer two questions:

  1. Can the list itself be null?
  2. Can an item inside the list be null?

For example:

tags: [String!]!

means the API always returns a list, and every item in that list is a non-null string.

Understanding this syntax prevents many generated-client and UI surprises.

Fields can take arguments

Arguments make fields configurable:

type Query {
  product(id: ID!): Product
  products(first: Int!, after: String): ProductConnection!
}

A client can call:

query {
  products(first: 20) {
    nodes {
      id
      name
    }
  }
}

Arguments are typed and validated before normal execution.

Do not use one giant generic filter: JSON argument unless there is a genuine reason to give up schema-level discoverability and validation.

Variables separate query structure from runtime values

Instead of embedding values directly in the query:

query {
  product(id: "p42") {
    name
  }
}

use variables:

query GetProduct($id: ID!) {
  product(id: $id) {
    name
  }
}

with variables:

{
  "id": "p42"
}

Variables are important because they:

  • keep operation documents stable
  • avoid string-building query text
  • enable typed input validation
  • work better with persisted operations and tooling

Treat GraphQL queries as structured documents, not templated strings assembled through concatenation.

Operation names matter

This operation has a name:

query ProductPage($id: ID!) {
  product(id: $id) {
    id
    name
  }
}

Naming operations improves:

  • server logs
  • traces
  • metrics
  • client debugging
  • persisted-operation management

A production system that logs only POST /graphql without operation names becomes difficult to observe.

Require meaningful operation names for first-party clients when practical.

Fragments reuse field selections

Fragments let clients define reusable selections:

fragment ProductSummary on Product {
  id
  name
  price
}

query ProductPage($id: ID!) {
  product(id: $id) {
    ...ProductSummary
  }
}

They are especially useful in component-oriented frontends where several UI components depend on a stable subset of fields.

Fragments affect query composition, not server-side storage or execution guarantees.

A large fragment can still request expensive data.

Aliases let one query request the same field differently

Aliases rename fields in the response:

query CompareProducts {
  first: product(id: "p1") {
    name
  }
  second: product(id: "p2") {
    name
  }
}

Result:

{
  "data": {
    "first": { "name": "Keyboard" },
    "second": { "name": "Mouse" }
  }
}

Aliases are useful, but public GraphQL services should still limit abusive query shapes such as excessive aliases that multiply backend work.

Interfaces and unions model polymorphism

An interface describes fields shared by several object types:

interface Node {
  id: ID!
}

A union represents one of several possible types:

union SearchResult = Product | User | Article

Clients can use inline fragments to request type-specific fields:

search(text: "keyboard") {
  ... on Product {
    id
    name
  }
  ... on Article {
    id
    title
  }
}

This gives GraphQL a structured way to represent heterogeneous results without returning untyped generic objects.

Input objects structure complex arguments

For mutations and complex filters, input types group related values:

input CreateOrderInput {
  customerId: ID!
  items: [OrderItemInput!]!
}

input OrderItemInput {
  productId: ID!
  quantity: Int!
}

This is clearer than exposing a long list of unrelated scalar arguments.

Input object types are distinct from output object types because input and output semantics evolve differently.

Queries read; mutations request state changes

GraphQL defines root operation types such as Query and Mutation.

A mutation can look like:

mutation CreateOrder($input: CreateOrderInput!) {
  createOrder(input: $input) {
    order {
      id
      status
    }
    userErrors {
      field
      message
    }
  }
}

GraphQL guarantees serial execution of top-level mutation fields within one mutation operation.

That does not mean your entire mutation is automatically a database transaction.

Your application still decides transaction boundaries, idempotency, authorization, and side-effect behavior.

Mutations should return useful canonical state

A good mutation response lets the client reconcile its local state without immediately making another query.

For example, return:

  • created object's ID
  • current status
  • server-generated timestamps
  • validation/business errors
  • changed version fields

Avoid mutation responses that say only:

{ "success": true }

when the client immediately needs another round trip to learn what the server actually created.

GraphQL has subscriptions, but transport is separate

The GraphQL language includes subscription as an operation type.

Subscriptions represent an ongoing stream of results after an initial operation.

However, the core GraphQL specification intentionally does not mandate one particular network transport for all GraphQL operations.

In practice, GraphQL subscriptions often use WebSockets or another streaming mechanism with an ecosystem-specific protocol.

Do not assume enabling GraphQL automatically gives your application durable realtime messaging.

Reconnect, authentication, event retention, and backpressure still require architecture.

Resolvers connect schema fields to application data

A resolver is application code that produces a field value.

Conceptually:

Query.product
  → look up product

Product.seller
  → load seller

Product.reviews
  → load reviews

Resolvers can read from:

  • SQL/NoSQL databases
  • caches
  • REST APIs
  • gRPC services
  • files
  • computed functions

The schema is the public contract; resolvers are one implementation layer behind it.

The default resolver can handle simple object fields

Many GraphQL server libraries can automatically resolve a field from an object property when names match.

For example, if a parent value contains:

{
  "id": "p42",
  "name": "Keyboard"
}

then fields id and name may need no custom resolver.

Use explicit resolvers when fetching, transforming, authorizing, or composing data rather than adding boilerplate to every scalar field.

GraphQL validates before normal execution

A request is checked against the schema before execution.

Invalid examples include:

  • requesting a field that does not exist
  • passing the wrong argument type
  • omitting a required argument
  • selecting subfields from a scalar
  • failing to select subfields from an object

This moves many API errors from runtime guessing into predictable schema validation.

Typed client tooling can catch some of these errors even before a request reaches production.

Execution can produce partial data and errors

A GraphQL response can contain both:

{
  "data": {
    "product": {
      "name": "Keyboard",
      "seller": null
    }
  },
  "errors": [
    {
      "message": "Seller service unavailable"
    }
  ]
}

This model allows one part of a query to succeed while another field fails, depending on nullability.

Clients should therefore inspect the GraphQL result rather than assuming an HTTP-level success means every field resolved successfully.

Introspection makes the schema queryable

GraphQL includes an introspection system.

Tools can ask a service about:

  • types
  • fields
  • arguments
  • directives
  • deprecations

This powers:

  • documentation explorers
  • IDE autocomplete
  • generated client types
  • schema tooling

Introspection is one reason GraphQL developer tooling can feel unusually rich.

Security still depends on authorization and query controls; hiding schema metadata is not a replacement for protecting data.

Deprecation helps schemas evolve

GraphQL supports deprecation metadata for fields, arguments, input fields, and enum values in the current specification.

Example:

type User {
  id: ID!
  displayName: String!
  oldName: String @deprecated(reason: "Use displayName")
}

A common evolution path is:

add replacement field
mark old field deprecated
migrate clients
observe remaining usage
remove only after safe compatibility window

Do not rename/remove fields casually merely because schema tooling can regenerate clients.

Mobile and third-party clients can remain on older versions for a long time.

GraphQL does not define a database model

This is a frequent misconception.

GraphQL object types do not need to map one-to-one to database tables.

A field can aggregate several sources:

Product.price       → PostgreSQL
Product.stock       → inventory service
Product.rating      → analytics store
Product.recommendations → recommendation service

That flexibility is powerful, but it can also create hidden latency waterfalls.

The GraphQL API needs batching, caching, deadlines, and tracing when fields call remote dependencies.

GraphQL does not automatically eliminate N+1 queries

A naive nested resolver can trigger one backend query per parent object.

Example:

load 100 orders
then load customer separately for each order

Common solutions include:

  • request-scoped batching
  • DataLoader-style patterns
  • joins/prefetching
  • smarter data-source APIs

The GraphQL query may look elegant while the backend performs hundreds of calls.

Measure execution, not just response syntax.

GraphQL needs bounded lists and query cost controls

Because clients can compose query shapes, production servers should prevent unbounded expensive requests.

Useful controls include:

  • list pagination
  • maximum page sizes
  • depth limits
  • query complexity/cost limits
  • operation timeouts
  • rate limits weighted by cost
  • alias/fragment abuse protections where necessary

The GraphQL-over-HTTP specification explicitly allows additional validation such as depth or complexity limits.

A single /graphql request should not be allowed to request the entire business dataset recursively.

GraphQL can be transported over HTTP

GraphQL itself is transport-independent, but HTTP is the common transport.

The GraphQL-over-HTTP draft defines media types including:

application/json
application/graphql-response+json

For modern clients, Accept should include application/graphql-response+json.

Queries can be sent with HTTP GET using URL-encoded GraphQL request parameters, while POST is commonly used for request bodies and mutations.

The transport specification and GraphQL language specification solve different layers of the system.

GraphQL is not automatically POST-only

Read-only queries may use HTTP GET under GraphQL-over-HTTP rules.

That can help with:

  • browser/network visibility
  • CDN caching strategies
  • persisted-query URLs

Large query text or sensitive variable values can make GET undesirable, so real deployments often use a mixture of GET, POST, and persisted operations.

Do not confuse one framework's default with the GraphQL protocol model itself.

GraphQL is not automatically faster than REST

GraphQL can reduce unnecessary client requests or fields, but performance depends on implementation.

One GraphQL request might trigger:

1 database query

or:

12 downstream RPCs
500 SQL queries
large resolver CPU work

Likewise, one REST endpoint can be extremely efficient or poorly designed.

Measure backend work, latency, payload size, and cache behavior before making performance claims.

What GraphQL is especially good at

GraphQL is attractive when:

  • several UI clients need different shapes of connected data
  • a typed schema improves frontend/backend collaboration
  • clients should select fields explicitly
  • API aggregation hides several backend sources
  • code generation and introspection improve developer experience

What GraphQL does not solve automatically

You still need to design:

  • authentication
  • authorization
  • pagination
  • caching
  • N+1 prevention
  • query complexity limits
  • mutation idempotency
  • database transactions
  • retries/deadlines
  • realtime transport
  • observability
  • schema compatibility

A GraphQL layer can centralize these concerns, but it does not remove them.

Beginner checklist

When learning GraphQL, make sure you understand:

  1. The schema defines available types and operations.
  2. Clients explicitly select response fields.
  3. Scalars are leaf values; objects need sub-selections.
  4. ! controls nullability.
  5. Variables carry runtime input values.
  6. Fragments reuse selection sets.
  7. Queries read data; mutations request changes.
  8. Resolvers produce field values from backend data sources.
  9. Requests are validated against the schema before normal execution.
  10. Responses can contain both data and errors.
  11. Introspection powers much of the tooling ecosystem.
  12. The language does not mandate one database or transport.

GraphQL is easiest to understand as a typed contract between clients and an execution engine: the schema describes what can be requested, the operation describes what this client needs now, and resolvers connect that request to real application data.

Official references

Tags:GraphQLAPIsWeb DevelopmentType SystemsBackend Development
AI & AGENT ENGINEERING

BUILDING AN AI PRODUCT?

Talk to Lofingo about custom autonomous AI agents, enterprise workflow automation, and scalable software systems.

EXPLORE AI SERVICES →