When GraphQL in PHP Outperforms REST for Business Applications
GraphQL has been marketed as a direct replacement for REST APIs, which leads to unnecessary adoption in cases where it does not fit. The more useful question is not whether GraphQL is better than REST in general, but when it is the right tool for a specific PHP application. For some business application backends, GraphQL solves real problems that REST creates. For others, it adds complexity without meaningful benefit.
This guide covers what GraphQL actually is, how it differs from REST in practice, and the specific scenarios where it makes sense for a PHP backend. If you are working on a REST API and want a practical starting point, I have written a separate guide on building a simple REST API in PHP from scratch that covers the fundamentals.
What GraphQL Actually Is
GraphQL is a query language for APIs and a runtime for executing those queries against your data. The client describes exactly what data it needs, sends a single request to the server, and receives precisely that data in the response. This is fundamentally different from REST, where each resource is identified by a URL and the server returns a fixed data structure regardless of what the client actually needs.
A REST endpoint for an invoice might return the full invoice record with all fields, whether the client needs them all or not. A GraphQL query for the same invoice specifies exactly which fields are required:
query {
invoice(id: "1234") {
number
date
client_name
total
line_items {
description
quantity
unit_price
}
}
}
The response contains only the requested fields. The client never receives data it did not explicitly ask for, which reduces payload size and simplifies client-side data handling.
The Overfetching and Underfetching Problem
REST APIs commonly suffer from two related issues: overfetching and underfetching. Overfetching means the endpoint returns more data than the client needs, wasting bandwidth and increasing response payload size. Underfetching means a single screen or feature requires multiple API calls because no single endpoint provides all the needed data.
Consider a dashboard that needs to show an order, the customer details, and the shipping status. With a REST API, this typically requires at minimum three separate API calls if each resource has its own endpoint. With GraphQL, one query fetches all three:
query {
order(id: "5678") {
order_number
status
customer {
name
email
}
shipments {
tracking_number
carrier
}
}
}
This matters most for applications with complex data requirements and multiple client types, such as a web admin panel, a mobile app, and an external integration all consuming the same API. Each client can request only the fields it needs without the backend maintaining separate endpoints for each use case.
When GraphQL Is the Right Choice for PHP
GraphQL makes sense for PHP applications when the client needs flexibility in what data it receives. The most common scenarios where this applies in business applications include:
- Multi-client applications: When the same backend serves a web admin panel, a customer portal, and a mobile app, each client needs different fields from overlapping data models. Rather than maintaining separate REST endpoints for each client, one GraphQL schema serves all three with different queries.
- Complex reporting and data export: When users need to define which fields and related data appear in a report or data export, GraphQL queries let the client specify exactly what is needed rather than building fixed report endpoints for every permutation of fields.
- Aggregated data from multiple sources: When a single API call needs to pull data from multiple database tables, external APIs, or third-party services, GraphQL resolves all of them in one request rather than requiring the client to make multiple calls and assemble the results.
- Rapidly evolving frontends: When the frontend team works independently and needs to request new fields without waiting for backend changes to existing endpoints, GraphQL schemas allow frontend teams to query exactly what they need without backend coordination for every change.
When REST Is Still Better
For simpler PHP applications with well-defined, stable data requirements, REST is usually the better choice. The overhead of designing and maintaining a GraphQL schema does not always pay off:
- CRUD operations on single resources: Creating, reading, updating, and deleting a single entity type maps naturally to REST. POST /orders, GET /orders/123, PUT /orders/123, DELETE /orders/123 is straightforward and familiar to most developers.
- Public APIs with caching requirements: GraphQL POST requests are difficult to cache at the HTTP level because each query can be different. REST endpoints with stable URLs can be cached by CDN edge nodes or reverse proxies, which improves performance for public-facing APIs.
- Simple integrations: When another system needs to read or write one specific type of data, a simple REST endpoint is easier to implement, document, and consume than a full GraphQL schema.
- Teams without GraphQL experience: GraphQL schemas and resolver logic require understanding that REST endpoints do not. If the team is not familiar with GraphQL, the initial implementation will likely have design issues that cause problems later.
Implementing GraphQL in PHP
The most common GraphQL library for PHP is webonyx/graphql-php. Install it with Composer:
composer require webonyx/graphql-php
Define a schema for the data your API will expose. The schema defines the types available and the queries clients can run:
use GraphQL\Type\Schema;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
$queryType = new ObjectType([
'name' => 'Query',
'fields' => [
'invoice' => [
'type' => Type::string(),
'args' => [
'id' => Type::nonNull(Type::string())
],
'resolve' => function($root, $args) {
return getInvoiceById($args['id']);
}
]
]
]);
$schema = new Schema(['query' => $queryType]);
Handle incoming GraphQL requests by reading the request body and executing the query:
use GraphQL\GraphQL;
use GraphQL\Server\StandardServer;
$rawInput = file_get_contents('php://input');
$input = json_decode($rawInput, true);
$query = $input['query'] ?? '';
$variables = $input['variables'] ?? [];
$result = GraphQL::executeQuery($schema, $query, null, null, $variables);
$output = $result->toArray();
header('Content-Type: application/json');
echo json_encode($output);
Designing the Schema for Business Data
The GraphQL schema should mirror the domain model of the business application. For an invoicing system, the schema has types for Invoice, Customer, LineItem, and Payment. Each type exposes fields that clients can query, and relationships between types are expressed as fields that return other types:
type Invoice {
id: ID!
number: String!
date: String!
status: String!
customer: Customer!
line_items: [LineItem!]!
payments: [Payment!]!
subtotal: Float!
tax: Float!
total: Float!
}
type Customer {
id: ID!
name: String!
email: String!
phone: String
addresses: [Address!]!
}
type LineItem {
id: ID!
description: String!
quantity: Int!
unit_price: Float!
total: Float!
}
The exclamation mark (!) indicates non-nullable fields. An ID! means the field is always present and never null. A [LineItem!]! means the array is always present and contains non-null LineItem objects.
Authentication and Authorisation
GraphQL does not define an authentication mechanism. It sits on top of whatever authentication the application already uses. The most common approach is to validate a session or JWT before the GraphQL resolver runs and pass the authenticated user through the context:
use GraphQL\GraphQL;
$context = ['user' => authenticateRequest()];
$result = GraphQL::executeQuery(
$schema,
$query,
null,
$context,
$variables
);
function resolveInvoice($root, $args, $context) {
$user = $context['user'];
if (!$user) {
throw new \Exception('Not authenticated');
}
$invoice = getInvoiceById($args['id']);
if ($invoice->customer_id !== $user->customer_id) {
throw new \Exception('Not authorised');
}
return $invoice;
}
Field-level authorisation checks inside resolvers ensure that users can only access data they own. This is harder to get wrong than a REST API where the authorisation check depends on middleware being correctly applied to every endpoint. With resolvers, the authorisation logic is co-located with the data access.
Performance Considerations
GraphQL resolvers can suffer from N+1 query problems. If a query requests a list of invoices and each invoice resolver makes a separate database query to fetch the customer, a list of 100 invoices generates 101 queries to the database. Use a data loader pattern to batch and cache database queries:
use GraphQL\Async\AbstractDataLoader;
class CustomerLoader extends AbstractDataLoader {
protected function fetch(array $keys) {
$customers = getCustomersByIds($keys);
$customerMap = [];
foreach ($customers as $customer) {
$customerMap[$customer->id] = $customer;
}
return array_map(
fn($id) => $customerMap[$id] ?? null,
$keys
);
}
}
The data loader batches multiple requests for customers into a single database query, regardless of how many times the customer field is referenced in the GraphQL query. This significantly reduces database load for complex queries with repeated relationships.
API Design Considerations
Whether you choose GraphQL or REST, the underlying API design principles matter for long-term maintainability. A well-designed API anticipates how clients will use the data and structures endpoints or types to match real-world usage patterns. For more on this topic, see my guide on simple API design for business web applications, which covers principles that apply to both approaches.
Making the Decision
The choice between GraphQL and REST for a PHP application depends on the specific requirements of the project rather than general popularity. GraphQL excels when serving multiple clients with different data needs, when aggregation from multiple sources is common, or when frontend teams need flexibility to request new fields without backend changes. REST remains the simpler choice for CRUD-focused applications, public APIs that benefit from HTTP caching, or teams without GraphQL experience.
If your application fits the GraphQL use case, use webonyx/graphql-php to implement the schema and resolver layer, apply authentication at the resolver level with proper field-level authorisation checks, and address N+1 query problems with a data loader pattern. If it does not fit, REST will serve you better with less complexity.
If you need help evaluating whether GraphQL is the right choice for your PHP application or want a practical review of your current API setup, you can get in touch with details of your requirements and current architecture.