What Is GraphQL? A Clear Guide for APIs and Data Fetching
Learn what GraphQL is, how queries, mutations, and schemas work, and when to use it instead of REST—plus practical pros, cons, and examples.

What GraphQL Is (and What It Isn’t)
GraphQL is a query language and a runtime for APIs. Put simply: it’s a way for an app (web, mobile, or another service) to ask an API for data using a clear, structured request—and for the server to return a response that matches that request.
The problem it solves
Many APIs force clients to accept whatever a fixed endpoint returns. That often leads to two issues:
- Over-fetching: downloading fields you don’t use.
- Under-fetching: making multiple requests to assemble one screen.
With GraphQL, the client can request exactly the fields it needs, no more and no less. This is especially useful when different screens (or different apps) need different “slices” of the same underlying data.
Where GraphQL “lives”
GraphQL typically sits between client apps and your data sources. Those data sources might be:
- databases
- existing REST services
- third‑party APIs
- microservices
The GraphQL server receives a query, figures out how to fetch each requested field from the right place, and then assembles the final JSON response.
A quick mental model
Think of GraphQL as ordering a custom-shaped response:
- The client describes the shape of the data it wants.
- The server returns data in that exact shape (when possible).
What GraphQL isn’t
GraphQL is often misunderstood, so here are a few clarifications:
- It’s not a database (it doesn’t store your data).
- It’s not automatically faster (it can reduce unnecessary data transfer, but server work still matters).
- It’s not “REST 2.0” (it’s an alternative API approach with different strengths and tradeoffs).
If you keep that core definition—query language + runtime for APIs—you’ll have the right foundation for everything else.
Why GraphQL Was Created
GraphQL was created to solve a practical product problem: teams were spending too much time making APIs fit real UI screens.
Traditional endpoint-based APIs often force a choice between shipping data you don’t need or making extra calls to get what you do need. As products grow, that friction shows up as slower pages, more complicated client code, and painful coordination between frontend and backend teams.
The pain points GraphQL targets
Over-fetching happens when an endpoint returns a “full” object even if a screen only needs a few fields. A mobile profile view might only need a name and avatar, but the API returns addresses, preferences, audit fields, and more. That wastes bandwidth and can hurt user experience.
Under-fetching is the opposite: no single endpoint has everything a view needs, so the client must make multiple requests and stitch results together. That adds latency and increases the chances of partial failures.
Evolving APIs without constant version bumps
Many REST-style APIs respond to change by adding new endpoints or versioning (v1, v2, v3). Versioning can be necessary, but it creates long-lived maintenance work: old clients keep using old versions, while new features pile up elsewhere.
GraphQL’s approach is to evolve the schema by adding fields and types over time, while keeping existing fields stable. That often reduces the pressure to create “new versions” just to support new UI needs.
One API, many clients
Modern products rarely have just one consumer. Web, iOS, Android, and partner integrations all need different data shapes.
GraphQL was designed so each client can request exactly the fields it needs—without the backend creating a separate endpoint for every screen or device.
The GraphQL Schema: The API Contract
A GraphQL API is defined by its schema. Think of it as the agreement between the server and every client: it lists what data exists, how it’s connected, and what can be requested or changed. Clients don’t guess endpoints—they read the schema and ask for specific fields.
Schema basics: types, fields, relationships
The schema is made of types (like User or Post) and fields (like name or title). Fields can point to other types, which is how GraphQL models relationships.
Here’s a simple example in Schema Definition Language (SDL):
type User {
id: ID!
name: String!
posts: [Post!]!
}
type Post {
id: ID!
title: String!
body: String
author: User!
comments: [Comment!]!
}
type Comment {
id: ID!
text: String!
author: User!
post: Post!
}
Strong typing = validation before execution
Because the schema is strongly typed, GraphQL can validate a request before running it. If a client asks for a field that doesn’t exist (for example, Post.publishDate when the schema has no such field), the server can reject or partially fulfill the request with clear errors—without ambiguous “maybe it works” behavior.
Evolving safely over time
Schemas are designed to grow. You can usually add new fields (like User.bio) without breaking existing clients, because clients only receive what they ask for. Removing or changing fields is more sensitive, so teams often deprecate fields first and migrate clients gradually.
Queries: Asking for Exactly What You Need
A GraphQL API is typically exposed through a single endpoint (for example, /graphql). Instead of having many URLs for different resources (like /users, /users/123, /users/123/posts), you send a query to one place and describe the exact data you want back.
Picking fields (including nested data)
A query is basically a “shopping list” of fields. You can request simple fields (like id and name) and also nested data (like a user’s recent posts) in the same request—without downloading extra fields you don’t need.
Here’s a small example:
query GetUserWithPosts {
user(id: "123") {
id
name
posts(limit: 2) {
id
title
}
}
}
A predictable response shape
GraphQL responses are predictable: the JSON you get back mirrors the structure of your query. That makes it easier to work with on the frontend, because you don’t have to guess where data will appear or parse different response formats.
A simplified response outline might look like:
{
"data": {
"user": {
"id": "123",
"name": "Sam",
"posts": [
{ "id": "p1", "title": "Hello GraphQL" },
{ "id": "p2", "title": "Queries in Practice" }
]
}
}
}
If you don’t ask for a field, it won’t be included. If you do ask for it, you can expect it in the matching spot—making GraphQL queries a clean way to fetch exactly what each screen or feature needs.
Mutations: Writing Data Safely
Queries are for reading; mutations are how you change data in a GraphQL API—creating, updating, or deleting records.
The typical mutation flow
Most mutations follow the same pattern:
- Inputs: the client sends a structured input (often an
inputobject) such as the fields to update. - Validation & authorization: the server checks things like required fields, data formats, uniqueness, and whether the user is allowed to perform the action.
- Write: the server performs the database change (or calls another service).
- Payload/return type: the server returns a predictable result shape so the UI can update.
Why mutations return data
GraphQL mutations usually return data on purpose, rather than just “success: true”. Returning the updated object (or at least its id and key fields) helps the UI:
- update the screen immediately without an extra round-trip
- refresh cached data safely (common with clients like Apollo Client)
- show field-level errors in context
A common design is a “payload” type that includes both the updated entity and any errors.
A basic mutation example
mutation UpdateEmail($input: UpdateUserEmailInput!) {
updateUserEmail(input: $input) {
user {
id
email
}
errors {
field
message
}
}
}
For UI-driven APIs, a good rule is: return what you need to render the next state (for example, the updated user plus any errors). That keeps the client simple, avoids guessing what changed, and makes failures easier to handle gracefully.
Resolvers: How GraphQL Produces the Result
A GraphQL schema describes what can be asked for. Resolvers describe how to actually get it. A resolver is a function attached to a specific field in your schema. When a client requests that field, GraphQL calls the resolver to fetch or compute the value.
Resolvers are “field-level” functions
GraphQL executes a query by walking the requested shape. For each field, it finds the matching resolver and runs it. Some resolvers simply return a property from an object already in memory; others call a database, another service, or combine multiple sources.
For example, if your schema has User.posts, the posts resolver might query a posts table by userId, or call a separate Posts service.
Mapping schema fields to data sources
Resolvers are the glue between the schema and your real systems:
- Databases: SQL/NoSQL queries, stored procedures, ORM calls
- Services: REST/gRPC calls, internal microservices, third-party APIs
- Computed fields: totals, formatting, derived values
This mapping is flexible: you can change your backend implementation without changing the client query shape—so long as the schema stays consistent.
Performance: avoiding slow resolver chains (N+1)
Because resolvers can run per field and per item in a list, it’s easy to accidentally trigger many small calls (for example, fetching posts for 100 users with 100 separate queries). This “N+1” pattern can make responses slow.
Common fixes include batching and caching (e.g., collecting IDs and fetching in one query) and being intentional about which nested fields you encourage clients to request.
Where authorization and validation happen
Authorization is often enforced in resolvers (or shared middleware) because resolvers know who is asking (via context) and what data they’re accessing. Validation typically happens at two levels: GraphQL handles type/shape validation automatically, while resolvers enforce business rules (like “only admins can set this field”).
Errors and Partial Results
One thing that surprises people new to GraphQL is that a request can “succeed” and still include errors. That’s because GraphQL is field-oriented: if some fields can be resolved and others can’t, you may get partial data back.
What errors look like
A typical GraphQL response can contain both data and an errors array:
{
"data": {
"user": {
"id": "123",
"email": null
}
},
"errors": [
{
"message": "Not authorized to read email",
"path": ["user", "email"],
"extensions": { "code": "FORBIDDEN" }
}
]
}
This is useful: the client can still render what it has (for example, the user profile) while handling the missing field.
Field-level errors vs request-level failures
- Field-level errors happen during execution (a resolver throws, permission check fails, a downstream service times out). Other fields may still resolve.
- Request-level failures prevent execution (invalid JSON, malformed query, validation errors against the schema). In these cases,
datais oftennull.
User-friendly messages without leaking details
Write error messages for the end user, not for debugging. Avoid exposing stack traces, database names, or internal IDs. A good pattern is:
- A short, safe
message - A stable machine-readable
extensions.code - Optional metadata that’s safe (for example,
retryable: true)
Log the detailed error server-side with a request ID so you can investigate without exposing internals.
Tips for consistent handling across clients
Define a small error “contract” your web and mobile apps share: common extensions.code values (like UNAUTHENTICATED, FORBIDDEN, BAD_USER_INPUT), when to show a toast vs inline field errors, and how to handle partial data. Consistency here prevents every client from inventing its own error rules.
Subscriptions for Real-Time Updates
Subscriptions are GraphQL’s way to push data to clients as it changes, instead of making the client ask repeatedly. They’re typically delivered over a persistent connection (most commonly WebSockets), so the server can send events the moment something happens.
What subscriptions are (and how they work)
A subscription looks a lot like a query, but the result isn’t a single response. It’s a stream of results—each one representing an event.
Under the hood, a client “subscribes” to a topic (for example, messageAdded in a chat app). When the server publishes an event, any connected subscribers receive a payload that matches the subscription’s selection set.
Common use cases
Subscriptions shine when people expect changes instantly:
- Chat messages appearing in a room without refresh
- Notifications (mentions, order status changes, alerts)
- Live dashboards (system health, logistics, trading, sports scores)
Subscriptions vs polling
With polling, the client asks “Anything new?” every N seconds. It’s simple, but it can waste requests (especially when nothing changes) and still feels delayed.
With subscriptions, the server says “Here’s the update” immediately. That can reduce unnecessary traffic and improve perceived speed—at the cost of keeping connections open and managing real-time infrastructure.
When subscriptions are unnecessary complexity
Subscriptions aren’t always worth it. If updates are infrequent, not time-sensitive, or easy to batch, polling (or just re-fetching after user actions) is often enough.
They can also add operational overhead: connection scaling, auth on long-lived sessions, retries, and monitoring. A good rule: use subscriptions only when real-time is a product requirement, not just a nice-to-have.
Pros, Cons, and Practical Tradeoffs
GraphQL is often described as “power to the client,” but that power has costs. Knowing the tradeoffs up front helps you decide when GraphQL is a great fit—and when it might be overkill.
Where GraphQL shines
The biggest win is flexible data fetching: clients can request exactly the fields they need, which can reduce over-fetching and make UI changes faster.
Another major advantage is the strong contract provided by a GraphQL schema. The schema becomes a single source of truth for types and available operations, which improves collaboration and tooling.
Teams often see better client productivity because front-end developers can iterate without waiting for new endpoint variations, and tools like Apollo Client can generate types and streamline data fetching.
Common downsides to plan for
GraphQL can make caching more complex. With REST, caching is often “per URL.” With GraphQL, many queries share the same endpoint, so caching relies on query shapes, normalized caches, and careful server/client configuration.
On the server side, there are performance pitfalls. A seemingly small query can trigger many backend calls unless you design resolvers carefully (batching, avoiding N+1 patterns, and controlling expensive fields).
There’s also a learning curve: schemas, resolvers, and client patterns can be unfamiliar to teams used to endpoint-based APIs.
Security and operations
Because clients can ask for a lot, GraphQL APIs should enforce query depth and complexity limits to prevent abusive or accidental “too big” requests.
Authentication and authorization should be enforced per field, not only at the route level, since different fields may have different access rules.
Operationally, invest in logging, tracing, and monitoring that understand GraphQL: track operation names, variables (carefully), resolver timings, and error rates so you can spot slow queries and regressions early.
GraphQL vs REST: How They Differ
GraphQL and REST both help apps talk to servers, but they structure that conversation in very different ways.
How REST typically works
REST is resource-based. You fetch data by calling multiple endpoints (URLs) that represent “things” like /users/123 or /orders?userId=123. Each endpoint returns a fixed shape of data decided by the server.
REST also leans on HTTP semantics: methods like GET/POST/PUT/DELETE, status codes, and caching rules. That can make REST feel natural when you’re doing straightforward CRUD or working closely with browser/proxy caches.
How GraphQL works
GraphQL is schema-based. Instead of many endpoints, you usually have one endpoint, and the client sends a query describing the exact fields it wants. The server validates that request against the GraphQL schema and returns a response that matches the query shape.
This “client-driven selection” is why GraphQL can reduce over-fetching (too much data) and under-fetching (not enough data), especially for UI screens that need data from several related models.
When REST can be simpler
REST is often the better fit when:
- You’re doing file downloads/uploads (streaming, content types, range requests).
- Your API is mostly simple CRUD with predictable payloads.
- You rely heavily on HTTP caching at the edge and want maximum compatibility with existing tooling.
Hybrid approaches are common
Many teams mix both:
- Use GraphQL for UI-focused data fetching (web/mobile screens).
- Keep REST for specific services like auth callbacks, webhooks, file handling, or internal microservice endpoints.
The practical question isn’t “Which is better?” but “Which fits this use case with the least complexity?”
How to Design a GraphQL API (Beginner-Friendly Checklist)
Designing a GraphQL API is easiest when you treat it as a product for the people building screens, not as a mirror of your database. Start small, validate with real use cases, and expand as needs grow.
1) Start from UI screens (not tables)
List your key screens (e.g., “Product list”, “Product details”, “Checkout”). For each screen, write down the exact fields it needs and the interactions it supports.
This helps you avoid “god queries,” reduces over-fetching, and clarifies where you’ll need filtering, sorting, and pagination.
2) Model domain types, then add operations incrementally
Define your core types first (e.g., User, Product, Order) and their relationships. Then add:
- a small set of queries that match real screens
- a small set of mutations that match real user actions (“addToCart”, “placeOrder”)
Prefer business-language naming over database naming. “placeOrder” communicates intent better than “createOrderRecord”.
3) Naming and pagination basics
Keep naming consistent: singular for items (product), plural for collections (products). For pagination, you’ll usually choose one:
- Cursor-based: better for changing lists and “infinite scroll” (more stable)
- Offset-based: simpler, but can skip/duplicate items when data changes
Even at a high level, decide early because it shapes your API’s response structure.
4) Document as you build
GraphQL supports descriptions directly in the schema—use them for fields, arguments, and edge cases. Then add a few copy-paste examples in your docs (including pagination and common error scenarios). A well-described schema makes introspection and API explorers far more useful.
Getting Started: Tools, Testing, and Next Steps
Starting with GraphQL is mostly about picking a few well-supported tools and setting up a workflow you can trust. You don’t need to adopt everything at once—get one query working end-to-end, then expand.
Pick a server framework
Choose a server based on your stack and how much “batteries included” you want:
- Apollo Server: popular choice with a big ecosystem and great docs.
- GraphQL Yoga: lightweight, modern defaults, pleasant developer experience.
- NestJS: ideal if you already use Nest and want GraphQL integrated with its modules, DI, and patterns.
A practical first step: define a small schema (a couple of types + one query), implement resolvers, and connect a real data source (even if it’s a stubbed in-memory list).
If you want to move faster from “idea” to a working API, a vibe-coding platform like Koder.ai can help you scaffold a small full-stack app (React on the frontend, Go + PostgreSQL on the backend) and iterate on GraphQL schema/resolvers via chat—then export the source code when you’re ready to own the implementation.
Choose a client approach
On the frontend, your choice usually depends on whether you want opinionated conventions or flexibility:
- Apollo Client: widely used, strong caching and devtools.
- Relay: stricter patterns, often used with larger apps that want consistency.
- urql: smaller, composable, good for teams that want control.
If you’re migrating from REST, start by using GraphQL for one screen or feature, and keep REST for the rest until the approach proves itself.
Testing: schema + resolvers + integration
Treat your schema like an API contract. Useful layers of testing include:
- Schema validation (build the schema in CI; fail fast on invalid types)
- Resolver unit tests (mock data sources and verify edge cases and auth rules)
- Integration tests (run real GraphQL operations against a test server and database)
Next steps
To deepen your understanding, continue with:
- /blog/graphql-vs-rest
- /blog/graphql-schema-design
FAQ
What is GraphQL in simple terms?
GraphQL is a query language and runtime for APIs. Clients send a query describing the exact fields they want, and the server returns a JSON response that mirrors that shape.
It’s best thought of as a layer between clients and one or more data sources (databases, REST services, third-party APIs, microservices).
What problem does GraphQL solve compared to fixed REST endpoints?
GraphQL primarily helps with:
- Over-fetching: getting more fields than a screen needs.
- Under-fetching: needing multiple requests to assemble one view.
By letting the client request only specific fields (including nested fields), GraphQL can reduce extra data transfer and simplify client code.
What is GraphQL not?
GraphQL is not:
- A database (it doesn’t store data).
- Automatically faster (it can reduce wasted transfer, but server work still matters).
- “REST 2.0” (it’s a different API style with different tradeoffs).
Treat it as an API contract + execution engine, not a storage or performance magic bullet.
Why does GraphQL often use a single endpoint?
Most GraphQL APIs expose a single endpoint (often /graphql). Instead of multiple URLs, you send different operations (queries/mutations) to that one endpoint.
Practical implication: caching and observability usually key off the operation name + variables, not the URL.
What is a GraphQL schema, and why does it matter?
The schema is the API contract. It defines:
- Types (e.g.,
User,Post) - Fields on those types (e.g.,
User.name) - Relationships (e.g.,
User.posts)
Because it’s strongly typed, the server can validate queries before executing them and give clear errors when fields don’t exist.
How do GraphQL queries work?
GraphQL queries are read operations. You specify the fields you need, and the response JSON matches the query’s structure.
Tips:
- Name operations (e.g.,
query GetUserWithPosts) for better debugging and monitoring. - Use arguments for shaping results (e.g.,
posts(limit: 2)).
How do GraphQL mutations work, and why do they return data?
Mutations are write operations (create/update/delete). A common pattern is:
- Send an
inputobject - Validate + authorize on the server
- Perform the write
- Return a payload that includes the updated data and any errors
Returning data (not just success: true) helps the UI update immediately and keeps caches consistent.
What are resolvers, and where do auth and business rules usually live?
Resolvers are field-level functions that tell GraphQL how to fetch or compute each field.
In practice, resolvers might:
- Query a database
- Call another internal service
- Fetch from a third-party API
- Compute a derived value
Authorization is often enforced in resolvers (or shared middleware) because they know who is requesting what data.
How do you avoid common GraphQL performance issues like N+1 queries?
It’s easy to create an N+1 pattern (e.g., loading posts separately for each of 100 users).
Common mitigations:
- Batching (collect IDs and fetch in one call)
- Caching (per-request or shared caches)
- Being selective about expensive nested fields
Measure resolver timing and watch for repeated downstream calls during one request.
Why can a GraphQL response include both data and errors?
GraphQL can return partial data alongside an errors array. That happens when some fields resolve successfully and others fail (e.g., forbidden field, timeout in a downstream service).
Good practices:
- Use safe, user-friendly
messagestrings - Add stable
extensions.codevalues (e.g.,FORBIDDEN,BAD_USER_INPUT) - Log detailed errors server-side with a request ID
Clients should decide when to render partial data vs. treat the operation as a full failure.