What Is GraphQL? A Query-First API Style
A clear introduction to GraphQL — a single endpoint, exact-fields queries, a typed schema, and how it compares to REST. Learn when GraphQL fits and when it doesn't.
What you'll learn
- ✓What GraphQL is — and what problem it was designed to solve
- ✓How a single endpoint and a typed schema change API design
- ✓The three operation types: queries, mutations, subscriptions
- ✓How GraphQL compares to REST in practice
- ✓When GraphQL is worth the extra machinery — and when it isn't
Prerequisites
- •A rough idea of HTTP requests and responses
- •Comfort with JSON
- •Optional but useful: a quick read of What Is REST?
GraphQL is a query language for your API and a runtime that executes those queries against your data. It was invented at Facebook in 2012 and open-sourced in 2015. The headline idea: instead of the server defining many endpoints with fixed response shapes, the client asks for exactly the fields it needs from a single endpoint, and the server returns exactly that.
This post explains what that means in practice and when you should reach for it.
The shape of a GraphQL request
In REST, you call several URLs and stitch results together. In GraphQL, you POST a query to one URL:
POST /graphql
Content-Type: application/json
# The body is a JSON object with a "query" string
{
"query": "{ user(id: 42) { name email } }"
}
The server replies with a JSON object whose data field mirrors the shape of the query:
{
"data": {
"user": {
"name": "Alice",
"email": "alice@example.com"
}
}
}
No id field appeared in the request, so no id appeared in the response. That is the central promise: you get what you ask for, nothing more.
A more interesting query
GraphQL really shines when data is nested. A single request can walk through relationships:
# Fetch a user, their last 3 orders, and the items in each order
{
user(id: 42) {
name
orders(last: 3) {
id
total
items {
name
price
}
}
}
}
In a REST API, the same data would typically take three round trips: one for the user, one for their orders, one for items in each order. With GraphQL, the client describes the whole tree once and the server resolves it.
GraphQL vs REST in one table
| Concern | REST | GraphQL |
|---|---|---|
| Endpoints | Many (/users, /users/42/orders) | One (/graphql) |
| HTTP methods | GET, POST, PUT, PATCH, DELETE | Almost always POST |
| Response shape | Fixed by server | Shaped by the query |
| Versioning | URL or header (/v2/...) | Evolve the schema; deprecate fields |
| Caching | HTTP caching is built in | Needs client-side or per-resolver caching |
| Tooling | curl, Postman, browser DevTools | GraphiQL, Apollo, codegen |
Neither is “better.” They make different trade-offs.
Schema first
A GraphQL API is defined by a schema written in the Schema Definition Language (SDL). The schema is the source of truth for what the server can do.
# A typed description of the API surface
type User {
id: ID!
name: String!
email: String!
orders: [Order!]!
}
type Order {
id: ID!
total: Float!
items: [Item!]!
}
type Item {
name: String!
price: Float!
}
type Query {
user(id: ID!): User
users: [User!]!
}
A few syntax notes:
String,Int,Float,Boolean,IDare the built-in scalar types.!means non-null — the field is guaranteed to be present.[User!]!is a non-null list of non-null Users.Queryis the root type for read operations; every entry point lives here.
The schema is the contract. Clients can introspect it at runtime, which is what powers tools like GraphiQL — autocomplete, inline docs, and type-checked queries in the browser. We go deeper in GraphQL Schemas and Resolvers.
Operations: query, mutation, subscription
GraphQL has three operation types.
Query — read data
# A read operation
query GetUser {
user(id: 42) {
name
}
}
Mutation — change data
# A write operation; conventionally returns the modified resource
mutation CreatePost {
createPost(input: { title: "Hello", body: "World" }) {
id
title
}
}
By convention, mutations execute serially while queries execute in parallel. A mutation typically returns the new or updated object so the client can update its cache without a second round trip.
Subscription — real-time updates
# A long-lived stream — usually over WebSockets
subscription OnNewComment {
commentAdded(postId: "42") {
id
body
author { name }
}
}
Subscriptions push events from server to client. They are powerful but operationally heavier — most teams add them only when they have a clear use case (live chat, notifications, dashboards).
Variables
Hard-coding values into a query string is the GraphQL equivalent of SQL injection. Use variables instead:
# Declared once at the top of the operation
query GetUser($id: ID!) {
user(id: $id) {
name
email
}
}
// Variables are sent alongside the query
{
"query": "query GetUser($id: ID!) { user(id: $id) { name email } }",
"variables": { "id": "42" }
}
Variables are typed ($id: ID!), validated by the schema, and reusable.
Try it yourself. Open https://graphql.org/swapi-graphql/ (a public Star Wars GraphQL playground) and run this query:
{
allFilms(first: 3) {
films { title director releaseDate }
}
}Then add producers to the selection. Notice you didn’t have to change anything on the server — you just asked for a new field. That is the developer-experience promise of GraphQL.
Fragments
When the same fields are needed in multiple places, extract a fragment:
# Reusable selection set
fragment UserCard on User {
id
name
email
}
query {
me { ...UserCard }
user(id: 42) { ...UserCard }
}
Fragments are how large GraphQL codebases stay maintainable. UI components own their own fragments — each declares the data it needs and composes upward.
Errors
GraphQL responses always return HTTP 200, even when something went wrong. Errors land in a top-level errors array:
{
"data": { "user": null },
"errors": [
{
"message": "User 999 not found",
"path": ["user"],
"extensions": { "code": "NOT_FOUND" }
}
]
}
This catches REST developers by surprise. The HTTP layer says “request handled”; the body says what actually happened. Clients must check errors after every call.
Where GraphQL helps
GraphQL pays for its complexity when:
- The client’s data needs are varied and nested. A frontend that mixes users, orders, products, reviews, and recommendations on one screen benefits enormously from a single typed query.
- You have many client teams hitting one backend. Each team picks the fields it needs without backend changes.
- Over-fetching is a real cost. Mobile apps on slow networks particularly benefit from getting only what they will render.
- You want a strongly typed contract. Codegen tools turn the schema into typed client code, so your editor catches mismatches before runtime.
Where GraphQL hurts
It is not a free upgrade. Consider sticking with REST when:
- Your API is mostly simple CRUD. A GraphQL server adds layers (schema, resolvers, dataloader) that a few REST routes don’t need.
- You rely on HTTP caching, CDNs, or HTTP-aware tooling. Since everything is a POST to
/graphql, intermediate caches can’t help you for free. - Your consumers are partners or webhooks. Plain HTTP + JSON is more familiar and easier to call from any environment.
- You don’t have anyone who has run a GraphQL server in production. The operational learning curve (query depth limits, persisted queries, N+1 problems) is real.
Many companies ship both: REST for partner integrations and webhooks, GraphQL for their own frontend.
The N+1 problem (a teaser)
The most famous GraphQL footgun:
# Looks innocent — but every order may trigger its own SQL query
{
users {
name
orders { total }
}
}
If the server fetches orders per user naively, 100 users means 1 query for users plus 100 queries for orders. This is the N+1 problem, and the standard fix is a tool called DataLoader that batches requests within a single query execution. We unpack it in GraphQL Schemas and Resolvers.
Servers and clients you will hear about
On the server side, the popular choices today:
- Apollo Server (Node) — the default in many JavaScript shops
- GraphQL Yoga (Node) — lightweight, batteries-included
- graphql-ruby, graphene (Python), gqlgen (Go), Hot Chocolate (.NET)
On the client side:
- Apollo Client and urql for React
- Relay — Facebook’s own client, deeply integrated with fragments
- graphql-request — a minimal fetch wrapper for simple cases
You don’t need a heavy client. fetch('/graphql', { method: 'POST', body: JSON.stringify({ query }) }) works.
Try it yourself. Take a small REST API you’ve used (or the one from Build Your First REST API with Express) and sketch its GraphQL schema. Define type Todo, a Query with todos and todo(id: ID!), and a Mutation with createTodo, updateTodo, deleteTodo. You won’t write a server here — just the SDL. Notice how the surface shrinks from many endpoints to a handful of fields.
A pragmatic mental model
Stop thinking of GraphQL as a replacement for REST. Think of it as a query layer over your existing services. The resolvers behind the schema can call:
- Your existing REST endpoints
- A SQL database directly
- Microservices over gRPC
- Third-party APIs
The schema is the public face. The implementation under each field can be whatever you already have.
Recap
You now know:
- GraphQL is a schema-defined, query-first API style with a single endpoint
- Clients ask for exactly the fields they need; servers return that shape
- The three operations are query (read), mutation (write), subscription (real-time)
- The schema (SDL) is the contract; resolvers implement each field
- GraphQL excels with varied nested data; REST excels with simple CRUD and caching
- The classic pitfall is the N+1 problem, solved with batching tools like DataLoader
Next steps
The natural follow-up is to learn how schemas and resolvers fit together — how a typed declaration becomes running code.
Next: GraphQL Schemas and Resolvers
If you want to compare from the other side, revisit What Is REST? with GraphQL fresh in mind. The contrast makes both styles easier to reason about.
Questions or feedback? Email codeloomdevv@gmail.com.