Key Takeaways
- GraphQL lets clients select fields and relationships exposed by a typed API schema.
- The schema defines what clients can request; resolver functions supply the values.
- One operation can compose related data, but it doesn't guarantee fewer backend requests, smaller payloads, or lower latency.
- Production GraphQL needs deliberate caching, pagination, authorization, and query-cost controls.
GraphQL is a query language and specification for APIs. A server implementation validates and executes client operations against a typed schema, allowing clients to select the fields they need rather than accepting a single predefined response shape. That flexibility can improve data fetching, but it doesn't automatically make an API faster than REST.
What is GraphQL?
Think of GraphQL as a contract between your API and its consumers: the server publishes available types, fields, and arguments, and clients select from that contract. It isn't a database or an alternative to SQL: SQL queries a database, while GraphQL operations are executed by an API server that can call databases, services, or existing application code.
Implementations exist for different languages, including GraphQL Java and GraphQL.js for JavaScript. The underlying API model doesn't depend on your frontend framework.
GraphQL defines three operation types: queries read data, mutations express writes, and subscriptions provide a stream of results over time. A schema can support mutations and subscriptions in addition to queries.
What is GraphQL used for?
Consider a product with a mobile user directory and a web administration screen. The directory might need only user IDs and usernames; the administration screen might also need email addresses. GraphQL lets both clients select different fields from the same API contract.
Related-data composition is another reason to consider it. An order screen could request an order, its customer, and selected line-item fields in one operation, provided the schema exposes those relationships. Resolvers can assemble those values from different sources; the client doesn't need to know where each value lives. That moves integration work into the server rather than eliminating it.
Choose GraphQL when these differing client needs create real coordination costs. If a few straightforward resource endpoints already serve your application well, introducing a query layer may add more work than it removes.
How does GraphQL work?
Start with a client query. This operation asks for at most one user and selects only id and username:
query {
users(count: 1) {
id
username
}
}The server validates that operation against its schema. Save this schema definition language, or SDL, example as schema.graphql if you want to run the resolver example below:
type User {
id: ID!
username: String
email: String
}
type Query {
users(count: Int = 10): [User!]!
user(id: ID!): User
}Here, users promises a non-null list containing non-null users; an empty list is allowed. The user field requires a non-null ID argument but may return null, such as when no matching user exists. username and email are nullable. These are schema-design choices, not requirements imposed by GraphQL.
The example sets an omitted count argument to 10. Its upper limit is enforced by application code, not by the Int type. A successful JSON response to the query looks like this:
{
"data": {
"users": [
{
"id": "1",
"username": "superuser"
}
]
}
}The result follows the selected fields, so email isn't included. However, field selection doesn't specify the database query: a resolver could still load a complete database row before GraphQL constructs the response.
Client operation
|
v
Parse and validate against schema
|
v
Execute field resolvers --> databases / services / application code
|
v
Response: data, errors, or bothGraphQL schema and resolvers: a runnable example
A resolver supplies a field's value. It might read an object property, execute a database query, or call another API. If you're integrating external services, the usual concerns around using third-party APIs still apply.
To execute the example locally, use Node.js and npm. In a new directory, save the query above as query.graphql, save the schema as schema.graphql, and install GraphQL.js:
npm init -y
npm install graphqlCreate demo.mjs alongside those files:
import { readFileSync } from 'node:fs';
import { buildSchema, graphql } from 'graphql';
const read = (filename) =>
readFileSync(new URL(filename, import.meta.url), 'utf8');
const schema = buildSchema(read('./schema.graphql'));
const rows = [
{ id: '1', username: 'superuser', email: null },
];
const rootValue = {
users({ count }) {
if (!Number.isInteger(count) || count < 1 || count > 100) {
throw new Error('count must be an integer between 1 and 100');
}
return rows.slice(0, count);
},
user({ id }) {
return rows.find((row) => row.id === id) ?? null;
},
};
const result = await graphql({
schema,
source: read('./query.graphql'),
rootValue,
});
console.log(JSON.stringify(result, null, 2));
if (result.errors) process.exitCode = 1;Run it with:
node demo.mjsThis executes the operation locally; it doesn't start an HTTP server. GraphQL.js uses the root functions to resolve users and user, then its default field resolver reads matching properties such as id. The 100-item maximum is an illustrative application policy, not a GraphQL default.
Responses can contain partial data and errors
A successful response isn't the only possible shape. If a resolver for the nullable username field throws, the same query could produce partial data alongside an errors array:
{
"data": {
"users": [{ "id": "1", "username": null }]
},
"errors": [
{
"message": "Username unavailable",
"locations": [{ "line": 4, "column": 5 }],
"path": ["users", 0, "username"]
}
]
}Syntax and validation errors prevent execution and omit data. During execution, a failure on a non-null field propagates to its nearest nullable parent and can make the entire data value null. Clients must handle errors as well as usable data; an HTTP success status alone isn't enough.
GraphQL vs REST
The useful distinction isn't “flexible GraphQL versus inflexible REST.” REST APIs can provide filters, purpose-built endpoints, and sparse fieldsets that let clients request a subset of fields. JSON:API, for example, specifies sparse fieldsets and related-resource inclusion. GraphQL provides its own typed language for expressing selections and relationships.
| Decision point | GraphQL | REST |
|---|---|---|
| Response selection | Clients select fields from a typed schema. | Endpoints define representations; APIs can offer response-shaping options. |
| Related data | Schema relationships can be composed in one operation. | May use separate requests, included resources, or purpose-built endpoints. |
| HTTP caching | GET queries can be cached with suitable keys and policies. | Resource GET URLs fit conventional HTTP caching. |
Use the table as a design guide, not a performance ranking. Favor GraphQL when multiple clients need different combinations of related data. Favor REST when resource-oriented operations and straightforward HTTP behavior meet your needs with less machinery. Our GraphQL vs REST comparison explores that decision further.
Problems with GraphQL
Caching depends on transport and cache design
GraphQL doesn't require a particular transport or endpoint layout. A single HTTP endpoint is a common deployment convention, not what defines GraphQL. Over HTTP, queries may use GET or POST; mutations must not use GET.
POST-based operations don't fit ordinary URL-keyed caches as directly as GET requests. That doesn't make GraphQL uncacheable. Distinguish two separate approaches:
- HTTP/CDN caching: Cache eligible GET query responses using appropriate cache headers and keys. Persisted queries identify stored operations by a hash or ID, allowing shorter GET URLs. Account for variables and any identity or tenant context that affects the result.
- Client-side normalized caching: Store entities under stable identifiers so different operations can reuse them. Apollo Client's normalized cache is one implementation, not a GraphQL requirement or a substitute for CDN caching.
Neither approach removes the need to decide freshness, invalidation, and access boundaries. See Apollo's server-side caching guidance for a concrete HTTP/CDN implementation.
One client operation can hide N+1 backend requests
The N+1 problem occurs when one fetch loads a list and each item triggers another fetch for related data. A single GraphQL operation can therefore generate many database or service requests. Use joins or batch reads where appropriate; DataLoader provides batching and request-local memoization. Keep loaders request-scoped when access differs between users.
Measure backend calls and resolver latency, not just the number of browser requests. For Node services, pair this with broader Node.js performance optimization.
Lists need bounds and pagination
The sample's count limit only slices the beginning of the list; it doesn't let clients traverse the remaining results. For growing collections, add pagination and enforce a maximum page size on the server. A cursor is an opaque continuation token that tells the server where to resume.
A connection-style API could support this operation. It requires a separate schema exposing usersConnection; it isn't part of the runnable example:
# Illustrative operation for a separately defined connection schema.
query UserPage($after: String) {
usersConnection(first: 20, after: $after) {
nodes {
id
username
}
pageInfo {
endCursor
hasNextPage
}
}
}The client passes endCursor as the next request's after value while hasNextPage is true. These names are schema conventions, not built-in GraphQL keywords. Bound nested lists too; limiting only the outer collection leaves other expensive work unrestricted.
Expensive queries and authorization are server responsibilities
A valid operation isn't necessarily cheap or authorized. Put these controls around execution:
- Demand controls: Limit nesting depth, field breadth, and estimated query cost. Include list sizes in cost estimates; shallow operations can still be expensive.
- Rate limits and timeouts: Don't assume every request costs the same. Apply work-aware budgets and bound downstream execution.
- Authorization: Check access to objects and sensitive fields wherever they're reachable. Enforce policy in shared business logic called by resolvers, rather than relying on clients to omit restricted fields.
The GraphQL security guide explains demand controls, while the authorization guidance covers keeping permission rules consistent.
Schema growth adds coordination work
Responsibility is shared. Clients manage selections, variables, partial results, and cache updates; servers manage resolver efficiency, permissions, list limits, and API contracts. Review consumers before removing fields or changing argument and nullability contracts. The schema makes those contracts explicit, but it doesn't remove the work of maintaining them.
FAQ
Is GraphQL replacing REST APIs?
GraphQL is an alternative to REST, not a replacement. Choose it when client-specific field selection and related-data composition justify the additional server and client complexity.
How do I start using GraphQL?
Start with a small User schema and execute the query in the local example above. Once you understand the schema and resolver boundary, choose a server library and add authentication, pagination, and error handling before exposing real data.
Is GraphQL better for mobile app development?
It can help when a mobile client needs fewer fields than a desktop client. Actual bandwidth and latency depend on selected fields, resolver work, caching, and network behavior, so measure rather than assume an improvement.
Does GraphQL eliminate over-fetching and under-fetching?
No. Field selection can reduce unnecessary response fields, and related data can often be composed in one operation, but pagination and dependent workflows can still require additional requests. Resolvers may also fetch more backend data than the response exposes.
How does caching work with GraphQL?
GraphQL can use HTTP/CDN caching for eligible GET queries and client-side normalized caching for reusable entities. Both need deliberate cache keys, freshness rules, and access boundaries; neither is required by the GraphQL specification.
Further Reading
- Introduction to GraphQL — the API model and its relationship to existing code and data.
- Schemas and Types — fields, arguments, lists, and nullability.
- Getting Started with GraphQL.js — schema construction and local operation execution.
- GraphQL Responses — successful results, partial data, and errors.
- Serving GraphQL over HTTP — request methods, transport conventions, and response handling.
- GraphQL Pagination — slicing, cursors, and connection-style APIs.
- GraphQL Security — pagination limits, query cost, and other demand controls.
- Server-Side Caching — practical cache policies and CDN integration.
