GraphQL APIs in PHP

Reviewed & published by Brayan K

GraphQL is a query language for APIs that lets clients ask for exactly the data they need from a single endpoint. In this lesson you'll design a typed schema, write queries and mutations, back fields with resolvers using webonyx/graphql-php, and see why it solves REST's over- and under-fetching.

Part of the free PHP course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn in This Lesson

1️⃣ The Schema: A Typed Contract

Everything in GraphQL starts with the schema, written in the Schema Definition Language (SDL). It declares your types (like User and Post), their fields, and the relationships between them. Two special types are the entry points: Query lists every read operation, and Mutation lists every write. A trailing ! marks a field as non-nullable. Because the schema is typed, the server can validate any incoming query before running a line of resolver code.

# schema.graphql — written in the Schema Definition Language (SDL).
# The schema is a typed CONTRACT describing everything the API can do.

type User {
  id: ID!            # ! means non-nullable: always present
  name: String!
  email: String!
  posts: [Post!]!    # a list of Posts (the relationship)
}

type Post {
  id: ID!
  title: String!
  body: String!
}

# The special "Query" type lists every READ operation a client can run.
type Query {
  user(id: ID!): User      # fetch one user by id
  users: [User!]!          # fetch all users
}

# The special "Mutation" type lists every WRITE operation.
type Mutation {
  createUser(name: String!, email: String!): User!
}

This one document is the whole API's contract. It also self-documents: tools can read it to give clients autocompletion and validation, which is a big part of GraphQL's appeal.

2️⃣ Queries: Reading Exactly What You Need

A query reads data. The client writes the shape it wants — which fields, and how deep — and the server returns JSON in exactly that shape. Ask for three fields and you get three fields; ask for nested posts and they come back inside the user. This is the heart of GraphQL: the client controls the response shape, so there are no wasted bytes and no second request for related data.

# A QUERY reads data. The client asks for EXACTLY the fields it wants.
# Sent in the request body to the single /graphql endpoint.

query {
  user(id: 1) {     # call the user(id) field
    name            # ...and ask only for these three fields
    email
    posts {
      title         # nested: each post's title only
    }
  }
}

Notice how the response mirrors the request one-to-one. That predictability makes GraphQL responses easy to consume on the client.

3️⃣ Mutations: Changing Data

Reads use queries; anything that changes data uses a mutation — creating, updating, or deleting. The syntax is nearly identical, but the operation keyword is mutation, and it maps to a field on the special Mutation type. Usefully, a mutation returns data too: you pick which fields of the newly changed object to read back, so a create can immediately hand you the new id.

# A MUTATION changes data. Same endpoint, different operation keyword.
# It returns whatever fields you ask for from the newly created object.

mutation {
  createUser(name: "Grace", email: "grace@example.com") {
    id              # return the new id...
    name            # ...and name, to confirm the write
  }
}

Keeping reads (queries) and writes (mutations) separate is a clear, enforced convention — the schema makes it impossible to accidentally mutate data from a query.

4️⃣ Resolvers: Where PHP Does the Work

A schema describes what is possible; resolvers implement how. Every field is backed by a resolver function that returns its value — usually by reading a database, calling a service, or following a relationship. In PHP the standard engine is webonyx/graphql-php (composer require webonyx/graphql-php): you define types and their resolve callbacks, build a Schema, and call GraphQL::executeQuery() with the incoming query.

<?php
// Server side with webonyx/graphql-php — the core PHP GraphQL engine.
//   composer require webonyx/graphql-php
//
// Each FIELD is backed by a RESOLVER: a function that returns its value.

use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Schema;
use GraphQL\GraphQL;

$userType = new ObjectType([
    'name'   => 'User',
    'fields' => [
        'id'    => Type::nonNull(Type::id()),     // id: ID!
        'name'  => Type::nonNull(Type::string()), // name: String!
        'email' => Type::nonNull(Type::string()),
    ],
]);

$queryType = new ObjectType([
    'name'   => 'Query',
    'fields' => [
        'user' => [
            'type' => $userType,
            'args' => ['id' => Type::nonNull(Type::id())],
            // The RESOLVER: given the args, return the data for this field.
            'resolve' => function ($root, array $args) {
                return findUserById($args['id']);   // e.g. a DB lookup
            },
        ],
    ],
]);

$schema = new Schema(['query' => $queryType]);

// Execute an incoming query string against the schema:
$result = GraphQL::executeQuery($schema, $incomingQueryString);
echo json_encode($result->toArray());   // -> { "data": { "user": {...} } }
?>

For Laravel, Lighthouse builds on this engine with a schema-first, directive-driven style so you write far less of this wiring by hand — but knowing the raw resolver model first means you understand what it's doing.

5️⃣ Why GraphQL? Over- and Under-Fetching

The clearest reason to reach for GraphQL is the data-fetching problem REST runs into. With REST, each endpoint returns a fixed shape: a screen that needs a user plus their posts plus comments often means several requests (under-fetching), while an endpoint that returns every field forces clients to download data they'll never show (over-fetching). GraphQL collapses that into one request against one endpoint, returning precisely the fields asked for.

# THE CORE PROBLEM GraphQL SOLVES: over- and under-fetching.

# --- REST: under-fetching needs MANY round trips ---
GET /users/1          # returns the user (but not their posts)
GET /users/1/posts    # second request for the posts
GET /posts/9/comments # third request for the comments...

# --- REST: over-fetching returns fields you don't need ---
GET /users/1          # returns id, name, email, address, phone, settings...
                      # ...even if the page only shows the name.

# --- GraphQL: ONE request, EXACTLY the fields you want ---
query {
  user(id: 1) {
    name
    posts { title comments { body } }
  }
}                     # one endpoint, one round trip, no wasted bytes.

Now you try the two core skills — finishing a schema and writing a query. Fill in each ___ using the 👉 hint, then check it against the Output panel.

# 🎯 YOUR TURN — finish the schema for a simple "book" API.
# Fill in each blank marked ___ , keeping the types valid.

type Book {
  id: ID!
  title: ___           # 👉 a required text field, so: String!
  pages: Int!
}

# The READ operations live on the special "Query" type:
type ___ {             # 👉 the reserved read-root type name: Query
  book(id: ID!): Book
  books: [Book!]!
}

# ✅ A client can then run:  query { book(id: 1) { title pages } }

One more. Write the smallest possible query that asks for just one field.

# 🎯 YOUR TURN — write a query that fetches only a book's title.
# The endpoint is the same /graphql; you choose the operation + fields.

___ {                  # 👉 the read operation keyword: query
  book(id: 1) {
    ___                # 👉 ask for just one field: title
  }
}

# ✅ Expected response:
#   { "data": { "book": { "title": "Clean Code" } } }

Common Errors (and the fix)

Pro Tips

📋 Quick Reference — GraphQL in PHP

Concept / SnippetWhat It Means
type UserDefine an object type in SDL
String!A non-nullable (required) field
type QueryRoot type listing all read operations
type MutationRoot type listing all write operations
queryRead data, choosing exact fields
mutationCreate, update, or delete data
resolve => fn(...)Function that returns a field's value
webonyx/graphql-phpCore PHP GraphQL server engine
LighthouseSchema-first GraphQL for Laravel

Mini-Challenge: A GraphQL Todo API

No code is filled in this time — just a brief and an outline. Build it with webonyx/graphql-php: define a Todo type, a query that lists todos, and a mutation that adds one, then wire the single endpoint. This is the full read-and-write GraphQL loop you'll use in real services.

<?php
// 🎯 MINI-CHALLENGE: A minimal GraphQL "todo" API in PHP.
// No code is filled in — work from the steps below.
//
// 1. Install the engine:  composer require webonyx/graphql-php
// 2. Define a Todo ObjectType with fields:
//      id (ID!), title (String!), done (Boolean!)
// 3. Define a Query type with a field "todos" that returns [Todo!]!,
//    whose resolver returns an array of todos (from memory or a DB).
// 4. Define a Mutation type with a field "addTodo(title: String!): Todo!"
//    whose resolver creates and returns the new todo.
// 5. Build  new Schema(['query' => $queryType, 'mutation' => $mutationType]).
// 6. Read the incoming query from php://input, run
//      GraphQL::executeQuery($schema, $query, null, null, $variables),
//    and echo json_encode($result->toArray()).
//
// ✅ Expected: POSTing  query { todos { title done } }  returns your list;
//    POSTing  mutation { addTodo(title:"Ship it") { id } }  adds one.

// your code here
?>

🎉 Lesson Complete!

Practice quiz

How many HTTP endpoints does a typical GraphQL API expose?

  • One endpoint for everything (usually /graphql)
  • One per resource, like REST
  • Two: one read, one write
  • One per field

Answer: One endpoint for everything (usually /graphql). GraphQL exposes a single endpoint. The client describes exactly what it wants in the request body, rather than hitting many resource URLs.

In GraphQL, what is a 'query' used for?

  • Defining the schema
  • Creating new data
  • Reading (fetching) data
  • Deleting data

Answer: Reading (fetching) data. A query reads data. Operations that change data (create, update, delete) are mutations.

Which operation type changes data on the server?

  • fragment
  • mutation
  • subscription
  • query

Answer: mutation. Mutations create, update, or delete data. Queries are read-only by convention.

What is a resolver?

  • A caching layer
  • The HTTP router
  • A database table
  • A function that returns the value for a field

Answer: A function that returns the value for a field. Each field is backed by a resolver function that produces its value, often by reading a database or calling another service.

What problem does GraphQL solve compared to REST?

  • Over-fetching and under-fetching of data
  • It makes PHP run faster
  • It removes the need for a database
  • It encrypts traffic automatically

Answer: Over-fetching and under-fetching of data. REST endpoints return fixed shapes, causing over-fetching (too much) or under-fetching (needing multiple round trips). GraphQL lets the client request exactly the fields it needs.

Which PHP library is the de-facto core engine for building GraphQL servers?

  • symfony/console
  • webonyx/graphql-php
  • phpunit/phpunit
  • guzzlehttp/guzzle

Answer: webonyx/graphql-php. webonyx/graphql-php is the reference PHP implementation of the GraphQL spec, and most higher-level tools build on it.

Which package is the popular GraphQL solution specifically for Laravel?

  • Eloquent
  • Blade
  • Horizon
  • Lighthouse

Answer: Lighthouse. Lighthouse lets you build a GraphQL server in Laravel using a schema-first, directive-driven approach on top of webonyx/graphql-php.

What is the SDL in GraphQL?

  • A database driver
  • A query caching protocol
  • The Schema Definition Language used to describe types and fields
  • A PHP extension

Answer: The Schema Definition Language used to describe types and fields. The Schema Definition Language (SDL) is the typed, human-readable syntax for declaring object types, fields, queries, and mutations.

In the type 'type User { id: ID! name: String! }', what does the '!' mean?

  • The field is private
  • The field is non-nullable (required)
  • The field is deprecated
  • The field is a list

Answer: The field is non-nullable (required). A trailing ! marks a field as non-nullable: the server guarantees it will always return a value of that type.

Why is GraphQL strongly typed via its schema?

  • So the schema documents the API and validates every query before execution
  • To replace the need for HTTPS
  • So clients can skip authentication
  • To slow down responses

Answer: So the schema documents the API and validates every query before execution. The typed schema is a contract: it self-documents the API, powers tooling and autocompletion, and lets the server validate a query's shape before any resolver runs.

Continue this course

Frequently asked questions

What is the difference between GraphQL and REST?

REST exposes many endpoints, one per resource, and each returns a fixed shape — so you often over-fetch (get fields you don't need) or under-fetch (make several requests to assemble a screen). GraphQL exposes a single endpoint and a typed schema; the client sends a query describing exactly the fields and relationships it wants, and gets precisely that back in one round trip. REST is simpler and benefits from HTTP caching; GraphQL shines when clients need flexible, nested data.

What is a resolver and where does it run?

A resolver is a server-side function attached to a field in your schema. When a query asks for that field, the resolver runs and returns its value — typically by reading a database, calling an internal service, or following a relationship. Resolvers run on the server, compose naturally (a User resolver can call a posts resolver for nested data), and are where you enforce authorization and shape the data the client receives.

Which PHP libraries do I use to build a GraphQL API?

The foundation is webonyx/graphql-php, the reference PHP implementation of the GraphQL specification — it parses queries, validates them against your schema, and executes resolvers. For Laravel, Lighthouse builds on top of it with a schema-first, directive-driven workflow so you write less boilerplate. There are also clients and frameworks like API Platform that can serve GraphQL. For learning the fundamentals, webonyx/graphql-php is the place to start.

Why does GraphQL use a single endpoint instead of many?

Because the query itself describes what you want, the server doesn't need a different URL per resource — one endpoint (commonly POST /graphql) can answer any request the schema allows. This keeps the API surface tiny and stable: adding new fields or types doesn't add new routes, clients evolve their queries without the server changing URLs, and versioning is usually handled by deprecating fields rather than spinning up /v2 endpoints.

Does GraphQL replace my database or my framework?

No. GraphQL is only a query language and a runtime for your API layer — it sits in front of whatever data sources you already have. Your resolvers still read from a database (often via an ORM like Eloquent or Doctrine), call other services, or hit caches. You can run GraphQL alongside an existing REST API, and inside a framework like Laravel or Symfony. Think of it as a flexible, typed gateway to your data, not a storage engine.

Related lessons