REST, RESTful APIs, RPC, GraphQL, And gRPC
REST, RPC, GraphQL, and gRPC are different ways to expose operations across a network. PHP developers commonly consume REST and JSON RPC-style APIs, may build or consume GraphQL, and sometimes integrate with gRPC through generated clients, gateways, or sidecar services.
None of these are technologies you install or codebases you depend on. An API is an agreement: a set of conventions for how one system exposes data, or accepts changes to data, so another system can use it. REST is a named architectural style with defined constraints, GraphQL is a specification with a query language, and gRPC is a framework built on Protocol Buffers, but what you are really choosing in each case is a way of working. A team can even define its own conventions and publish them; the result is a perfectly usable API, just not one of the named styles.
Because an API is an agreement rather than an implementation, it is platform-independent and language-agnostic. A PHP backend can serve a Swift mobile app, a JavaScript frontend, and a Python data pipeline through the same contract, and none of them need to know the others exist. This is why shared standards matter: when everyone understands GET /v1/users or a GraphQL query the same way, systems written by strangers can interoperate without coordination. Older styles such as SOAP made the same bargain with XML envelopes and WSDL contracts, and many finance and government systems still speak it; the SOAP and XML APIs lesson covers maintaining those integrations.
The useful skill is not arguing that one style is always best. The useful skill is recognizing the interaction style, understanding its contract, and writing PHP code that handles its request shape, error model, timeout behavior, authentication, idempotency, and tooling.
Start With The Interaction Shape
Ask what the client is trying to do:
- retrieve or modify resource state
- issue a business command
- select a flexible graph of fields
- stream messages
- make typed internal service calls
The answer matters more than fashion. A public CRUD API, a checkout command, a mobile feed, and an internal high-volume service boundary may deserve different styles.
Every style still needs security, versioning, observability, failure handling, and compatibility testing. A protocol choice can make some contracts easier to express; it does not remove distributed-system work.
REST And HTTP Resources
REST-oriented HTTP APIs organize around resources identified by URLs and use standard HTTP methods.
<?php
declare(strict_types=1);
$restExamples = [
'list users' => 'GET /v1/users',
'get one user' => 'GET /v1/users/42',
'create user' => 'POST /v1/users',
'replace user' => 'PUT /v1/users/42',
'delete user' => 'DELETE /v1/users/42',
];
foreach ($restExamples as $operation => $request) {
echo $operation . ': ' . $request . PHP_EOL;
}
// Prints:
// list users: GET /v1/users
// get one user: GET /v1/users/42
// create user: POST /v1/users
// replace user: PUT /v1/users/42
// delete user: DELETE /v1/users/42
REST fits public APIs, CRUD-style resources, cacheable reads, browser-friendly tooling, OpenAPI documentation, and systems where HTTP status codes and intermediaries are important.
Use HTTP semantics honestly. GET should not create an export or send an email. 201 Created should mean creation. 202 Accepted should mean work was accepted but is not complete. 204 No Content should not contain JSON. 409 Conflict, 412 Precondition Failed, and 429 Too Many Requests are not interchangeable.
RPC For Explicit Commands
RPC means remote procedure call. The API is organized around actions or commands, such as calculateTax, sendInvoice, orders.cancel, or refundPayment.
<?php
declare(strict_types=1);
function rpcRequest(string $method, array $params): array
{
return [
'jsonrpc' => '2.0',
'method' => $method,
'params' => $params,
'id' => 'req_123',
];
}
echo json_encode(rpcRequest('orders.cancel', ['order_id' => 42]), JSON_THROW_ON_ERROR) . PHP_EOL;
// Prints:
// {"jsonrpc":"2.0","method":"orders.cancel","params":{"order_id":42},"id":"req_123"}
RPC fits action-heavy workflows that do not map cleanly to resources. It can be clearer to expose POST /orders.cancel than invent a fake resource just to avoid a verb.
The risk is inconsistency. A collection of action endpoints can drift into different naming, status, authentication, retry, and error conventions. Define an operation catalog, envelope, error codes, idempotency rules, and generated or documented clients.
GraphQL For Client-Selected Data
GraphQL exposes a typed schema and lets clients choose fields through queries. Most GraphQL APIs use a single HTTP endpoint such as POST /graphql. HTTP is the usual transport, but the specification is transport-agnostic: subscriptions commonly run over WebSockets, and nothing stops a GraphQL service from running over another protocol entirely. The same is true of REST in theory, though in practice both almost always mean HTTP.
The name is literal. The schema describes data as a graph of connected types, and executing a query means walking that graph: a product field resolves, then its reviews field resolves, then each review's author resolves. Each step runs a resolver function, and a resolver can fetch from anywhere: a SQL database, a document store, another REST API, or an in-memory cache. GraphQL has no special affinity for any particular database; it is a layer over whatever sources the resolvers reach.
The query syntax looks JSON-like, or vaguely like JavaScript, but it is neither. It is GraphQL's own query language, with its own grammar for fields, arguments, variables, fragments, and directives.
GraphQL defines three operation types. Queries read data. Mutations change data, the way POST, PUT, and DELETE do in REST. Subscriptions deliver a stream of results as server-side events occur. All three go through the same endpoint and schema; the operation type, not the URL or HTTP method, declares the intent.
The schema itself is written in GraphQL's schema definition language. It composes object types, scalars, enums, interfaces, unions, and input types, and marks required values with !, as in ID! above. Fields can carry a @deprecated directive, which is how a single evolving schema retires fields without a version bump. GraphQL is a specification governed by the GraphQL Foundation; the latest edition reviewed for this lesson is September 2025, which added OneOf input objects and schema coordinates.
<?php
declare(strict_types=1);
$graphqlRequest = [
'query' => <<<'GRAPHQL'
query Product($id: ID!) {
product(id: $id) {
id
name
price
}
}
GRAPHQL,
'variables' => ['id' => 'prod_123'],
];
echo array_key_first($graphqlRequest['variables']) . PHP_EOL;
// Prints:
// id
GraphQL is useful when clients need flexible data selection across related objects. A REST endpoint returns a representation the server chose, which tends to include fields a given client does not need, and assembling one screen may take several round trips to endpoints such as /customers/42 and /customers/42/orders. A GraphQL client asks for exactly the fields it wants across those relationships in one query, which reduces over-fetching and makes UI-driven data requirements explicit. This is a tendency rather than a law: REST was not designed to over-fetch, and many REST APIs offer sparse fieldsets such as ?fields=id,name or compound endpoints to narrow responses.
The schema is also introspectable: clients can query the API for its own types and fields. This powers interactive explorers such as GraphiQL and Apollo Sandbox, generated client types, and self-updating documentation, filling the role OpenAPI plays for REST. In production, introspection is commonly disabled or restricted so the full schema is not exposed to anonymous callers.
It also shifts complexity. A single request can trigger many resolvers and backend calls. Limit depth, complexity, aliases, and result size. Use batching to avoid N+1 queries. Cache carefully because clients can ask for many shapes; persisted operations, where the server accepts only pre-registered documents, restore cacheability and shrink the attack surface. Track operation names and error categories rather than only HTTP status.
GraphQL errors may appear inside a 200 OK response with partial data. The response body carries a data key for whatever resolved and an errors array describing what failed, each error optionally carrying machine-readable detail under extensions. Clients must understand this contract as well as transport failures.
Lists in GraphQL have their own pagination convention. Instead of ?page=2, many schemas follow the connection pattern: a field takes first and after arguments and returns edges containing node items plus cursor information. It is cursor pagination with standardized names, and the same trade-offs from the pagination lesson apply.
In PHP, GraphQL servers are usually built on webonyx/graphql-php, the long-standing spec implementation, either directly or through a framework layer: Lighthouse for Laravel, which is schema-first and driven by schema directives such as @paginate with custom resolver classes for anything the directives cannot express, or API Platform for Symfony, which can expose the same resources over both REST and GraphQL. Consuming a GraphQL API from PHP needs no special client at all: it is an HTTP POST with a JSON body holding query and variables, so curl or Guzzle is enough. Dedicated clients such as Apollo mainly earn their keep in frontends, where they add caching and generated types. At larger scale, federation tooling composes schemas owned by separate teams into one supergraph behind a single endpoint, which is how "one endpoint gathering from many sources" stays manageable across an organization.
gRPC And Protocol Buffers
gRPC uses service definitions and Protocol Buffer messages to generate typed clients and servers. Instead of hand-writing URLs, clients call generated methods.
service ProductService {
rpc GetProduct (GetProductRequest) returns (Product);
}
In PHP, gRPC usually means generated classes plus the gRPC extension or a compatible client library. It is common in controlled internal service-to-service systems where strict message contracts, efficient binary encoding, streaming, and generated clients matter more than browser readability.
Protocol Buffer field numbers are the durable wire identity. Do not reuse a removed field number for a different meaning. Adding optional fields is often compatible. Changing field type, oneof membership, required semantics, or default meaning needs careful review.
Clients should send deadlines. Servers should stop work when calls are cancelled where possible. Streaming adds backpressure, ordering, cancellation, and partial-delivery concerns.
Classification Is Only A Starting Point
Real systems mix styles. A Laravel application might expose REST to partners, GraphQL to its frontend, JSON RPC for internal admin commands, and gRPC to a search service.
<?php
declare(strict_types=1);
function likelyApiStyle(string $method, string $path, ?string $operation = null): string
{
if ($operation !== null && str_contains($operation, 'Service.')) {
return 'gRPC';
}
if ($path === '/graphql') {
return 'GraphQL';
}
if (strtoupper($method) === 'POST' && preg_match('#^/[a-z]+[A-Z][A-Za-z]*$#', $path)) {
return 'RPC';
}
return 'REST';
}
foreach ([
['GET', '/v1/products/123', null],
['POST', '/cancelOrder', null],
['POST', '/graphql', null],
['POST', '', 'ProductService.GetProduct'],
] as [$method, $path, $operation]) {
echo likelyApiStyle($method, $path, $operation) . PHP_EOL;
}
// Prints:
// REST
// RPC
// GraphQL
// gRPC
After classification, ask better questions: where is the contract documented, what are errors shaped like, how do retries work, are clients generated, and who owns compatibility?
Errors, Retries, And Idempotency
For every operation, define success, validation rejection, authentication failure, authorization denial, state conflict, rate limit, temporary dependency failure, and unexpected server failure.
Timeouts create ambiguity. The client may not know whether the server committed the operation. Resource creation can accept a client-generated identifier. Command-style APIs can accept idempotency keys. Long work can return an operation resource. gRPC can carry deadlines and status codes. GraphQL may need mutation idempotency rules despite using one endpoint.
These choices are part of the API contract, not an implementation detail.
Observability Across Styles
Record operation identity in a bounded form: HTTP route template, RPC method, GraphQL operation name, or gRPC service and method. Do not use raw URLs, arbitrary GraphQL documents, or user-provided identifiers as metric labels.
Propagate correlation or trace context across gateways and downstream calls. For GraphQL, one transport request can contain several resolver failures. For streaming gRPC, measure time to first message, stream duration, cancellations, and message counts. For asynchronous HTTP operations, trace the accepted request into the worker and status resource.
Preserve protocol semantics. A gateway that converts every error to HTTP 200 or one generic gRPC status makes dashboards simple while making clients unreliable.
Gateways And Migration
Many systems expose one style externally while using another internally. A GraphQL edge service may call REST services. A public REST API may call gRPC backends through a gateway. A legacy JSON RPC API may receive a REST facade during migration.
Gateways should preserve semantics deliberately. Map authentication failures, validation errors, conflicts, rate limits, and timeouts consistently. Do not collapse every backend error into a generic 500 or every GraphQL failure into transport 200 without a documented client contract.
During migration, support old and new styles long enough for consumers to move. Run contract tests for both paths and compare representative responses. A new API style is not an excuse to change field names, permissions, pagination, or retry behavior accidentally.
Versioning looks different in each style, but the discipline is the same: make compatibility promises explicit before clients depend on them. REST APIs often version through URL prefixes, media types, or headers. RPC APIs may version method names, envelopes, or service packages. GraphQL usually keeps one evolving schema, marks fields as deprecated, and measures usage before removal. gRPC and protobuf rely heavily on field-number compatibility, package names, and generated-client release discipline.
Do not confuse a new transport shape with a new product contract. Moving GET /orders/{id} behind a GraphQL field or gRPC method still requires the same tenant checks, money format, timestamp precision, and cancellation semantics. If the migration intentionally changes behavior, publish it as a versioned change and give consumers examples of both the old and new behavior.
Client ownership also affects the choice. A public API used by unknown integrators needs conservative compatibility and readable diagnostics. An internal gRPC boundary owned by two teams can move faster if both teams run generated-client compatibility checks. A frontend GraphQL schema can evolve quickly when the frontend and backend release together, but mobile clients with older app versions need longer deprecation windows.
When a team cannot name the supported client population, choose the more conservative contract. Unknown clients make removals, enum changes, and status-code changes harder to coordinate. Internal-only protocols still need documentation, but the migration plan can rely on known owners, shared CI, and scheduled releases.
Contract And Compatibility Testing
REST and HTTP RPC need request/response schema tests, status assertions, and OpenAPI or equivalent documentation. GraphQL needs schema checks, resolver integration tests, query complexity tests, deprecated-field monitoring, and generated type checks. gRPC needs protobuf compatibility checks and generated-client smoke tests across supported languages.
Run tests through the same gateways and proxies used in production where possible. Header handling, body size, compression, timeouts, and status mapping can differ from in-process handlers.
Choosing A Style
Use REST when resources, standard HTTP semantics, caching, public documentation, and many client types matter.
Use RPC when the domain is command-heavy and clear operation names are more honest than artificial resources.
Use GraphQL when clients genuinely need flexible graph-shaped selection and the team can operate schema governance, resolver performance, and query limits.
Use gRPC when service boundaries are controlled, generated clients are acceptable, efficient typed messages or streaming matter, and the infrastructure supports HTTP/2 and protobuf tooling.
Prototype one representative success and one representative failure before choosing. Measure payload shape, latency, debugging experience, generated code, observability, and deployment complexity.
What To Check
Before moving on, make sure you can:
- explain why an API is an agreement rather than a technology, and why that makes it platform- and language-independent
- recognize resource-oriented REST endpoints
- recognize action-oriented RPC endpoints
- describe how GraphQL requests, partial data, and errors differ from REST
- explain how resolvers walk the schema graph and why GraphQL is data-source agnostic
- name the three GraphQL operation types and what introspection provides
- explain why gRPC uses service definitions and generated clients
- define retry and idempotency behavior for side effects
- identify observability labels for each style
- choose a style based on consumers, operations, tooling, and infrastructure
What You Should Be Able To Do
After this lesson, you should be able to distinguish resource-oriented HTTP, command-oriented RPC, GraphQL selection, and gRPC services. You should be able to explain each style's contract, error model, retry concerns, operational costs, and compatibility testing needs.
Practice
Practice: Classify API Styles
Write a PHP function that classifies example API operations as REST, RPC, GraphQL, or gRPC.
Requirements
- Accept a method, path, and optional operation name.
- Classify
/graphqlas GraphQL. - Classify service-style operation names such as
ProductService.GetProductas gRPC. - Classify action-style
POSTendpoints such as/cancelOrderas RPC. - Treat ordinary resource paths such as
/v1/products/123as REST. - Return a short reason for the classification.
Show solution
This solution uses simple signals rather than pretending the boundary is always perfect. Many real APIs mix styles.
<?php
declare(strict_types=1);
function classifyApiStyle(string $method, string $path, ?string $operation = null): array
{
if ($operation !== null && preg_match('/^[A-Z][A-Za-z]+Service\.[A-Z][A-Za-z]+$/', $operation)) {
return ['style' => 'gRPC', 'reason' => 'The operation is service-method shaped.'];
}
if ($path === '/graphql') {
return ['style' => 'GraphQL', 'reason' => 'GraphQL commonly uses a single /graphql endpoint.'];
}
if (strtoupper($method) === 'POST' && preg_match('#^/[a-z]+[A-Z][A-Za-z]*$#', $path)) {
return ['style' => 'RPC', 'reason' => 'The endpoint is named like an action.'];
}
return ['style' => 'REST', 'reason' => 'The endpoint is resource-oriented.'];
}
$examples = [
classifyApiStyle('GET', '/v1/products/123'),
classifyApiStyle('POST', '/cancelOrder'),
classifyApiStyle('POST', '/graphql'),
classifyApiStyle('POST', '', 'ProductService.GetProduct'),
];
foreach ($examples as $result) {
echo $result['style'] . ': ' . $result['reason'] . PHP_EOL;
}
// Prints:
// REST: The endpoint is resource-oriented.
// RPC: The endpoint is named like an action.
// GraphQL: GraphQL commonly uses a single /graphql endpoint.
// gRPC: The operation is service-method shaped.
The classification helps you ask the next questions: where is the contract documented, how are errors represented, and whether the client code is handwritten or generated.
Practice: Choose An API Style
Choose a style for four product requirements.
Task
For each case, choose REST, RPC, GraphQL, gRPC, or a deliberate mix:
- public partner API for products, orders, and customers
- checkout operation that reserves stock, calculates tax, and authorizes payment
- frontend dashboard that needs different nested fields on different screens
- internal high-volume inventory service used by three controlled backend services
For each answer, justify contract documentation, error handling, client generation, observability, and retry/idempotency behavior.
Show solution
A public partner API is a strong REST candidate because resource URLs, HTTP status codes, OpenAPI documentation, pagination, caching, and broad client compatibility matter. Some action endpoints may still exist, but the core contract should be stable and easy to inspect.
Checkout is command-heavy. RPC or a REST command resource such as POST /checkouts/{id}/confirm can be honest. The design must document idempotency keys, timeout ambiguity, conflict responses, and a status resource or reconciliation path.
The dashboard may fit GraphQL if screens need flexible nested selections and the team can operate query complexity limits, resolver batching, authorization checks, and schema evolution. A simpler REST backend-for-frontend may be enough if the field combinations are predictable.
The internal inventory service may fit gRPC when consumers are controlled, generated clients are acceptable, and low-latency typed calls or streaming updates matter. Deadlines, protobuf compatibility, generated-client tests, and observability by service method are required.
Practice: Review GraphQL Risks
A team wants to replace several REST endpoints with one GraphQL endpoint for a product catalog.
Task
Write a review note covering:
- resolver N+1 risks
- query depth and complexity limits
- authorization at field and object level
- partial data and error handling
- caching differences from REST
- schema deprecation policy
- operation names and observability
- when REST may still be simpler
Show solution
GraphQL may help the catalog UI request exactly the fields it needs, but resolver design must avoid one query per product or category. Use batching and request-scoped loading for related data.
Set depth, complexity, alias, and result-size limits before exposing the endpoint. Authorization must be enforced at object and field boundaries, not only by hiding fields in the schema. Clients must handle partial data plus errors because GraphQL can return both in one response.
Caching is harder than caching GET /products/{id} because many query shapes share one endpoint. Consider persisted operations or application-level caches for common views. Track operation names, resolver latency, error categories, and downstream calls rather than only HTTP status.
Deprecate fields before removal and measure usage. REST may remain simpler for public product reads, file downloads, cache-heavy lists, and integrations where broad tooling and straightforward HTTP semantics matter more than flexible selection.