⏱️ Lectura: 13 min
You request a single field, “name”, from a REST API for users, and you get back a JSON with 40 fields you’re never going to touch: that waste of bandwidth and parsing time is called over-fetching, and it’s exactly the problem GraphQL was born to eliminate at Facebook, long before it went public in 2015.
📑 En este artículo
- TL;DR
- What GraphQL Is and Why It Matters
- How GraphQL Works in Detail
- Practical Examples
- Getting Started: Setting Up a GraphQL Server with Express
- Real-World Use Cases
- Common Mistakes and Best Practices
- Comparison with Alternatives
- Going Deeper: Persisted Queries, Federation, and Cost Control
- Frequently Asked Questions
- References
With GraphQL, the client no longer depends on the backend guessing what it needs: it writes a query that describes the exact shape of the response, and the server resolves it field by field against its data sources. This guide explains how it works under the hood, covering schema, resolvers, and query execution, and gets you writing your own GraphQL server in minutes.
TL;DR
- GraphQL exposes a single HTTP endpoint where the client defines the exact shape of the response with a declarative query.
- You’ll write a schema with type and Query, and get a working GraphQL server running in under 30 lines of code.
- You’ll understand the difference between query, mutation, and subscription, and when to use each.
- You’ll identify the N+1 problem in nested resolvers and solve it with DataLoader.
- You’ll compare GraphQL against REST and gRPC with concrete criteria to decide which fits your project.
- You’ll use introspection tools to explore a schema without reading external documentation.
- You’ll recognize the risks of malicious nested queries and how to mitigate them with depth limits.
What GraphQL Is and Why It Matters
GraphQL is a query language for APIs and a runtime for executing those queries against your existing data. Facebook created it in 2012 to solve a specific problem: the iOS app needed different data than Android and the web did, and maintaining a separate REST endpoint for each client became unmanageable. Facebook open-sourced the specification in 2015, and today it’s maintained by the GraphQL Foundation, with an official implementation in JavaScript and third-party implementations in nearly every server-side language.
The core difference with REST is who decides the shape of the response. In REST, the server defines fixed endpoints (/users/42, /users/42/posts) and each one returns a predetermined structure. In GraphQL there’s a single endpoint (usually /graphql), and the client sends a query that declares exactly which fields it wants, regardless of how many related resources the server has to traverse to resolve them.
This matters for two concrete reasons. First, it eliminates over-fetching (receiving extra fields) and under-fetching (having to make several chained calls to build a single screen). Second, it decouples client evolution from server evolution: adding a new field to the schema doesn’t break existing clients, because nobody receives fields they didn’t explicitly request.
The schema also works as a strongly typed contract between frontend and backend. Tools like graphql-code-generator read the schema and automatically generate TypeScript types for each query, so a type change in the backend breaks the frontend build instead of only failing once it hits production.
💭 Key point: GraphQL isn’t a database, and it doesn’t replace REST by decree: it’s a query layer on top of the data sources you already have, whether that’s SQL databases, REST microservices, or third-party APIs.
How GraphQL Works in Detail
A GraphQL server is built from two pieces: a schema, written in the Schema Definition Language (SDL), which declares what data types exist and what operations are available; and a set of resolvers, functions that know how to fetch the value of each field in the schema.
When a query arrives, the server processes it in three phases: parsing (converts the query text into a syntax tree), validation (checks that every requested field exists in the schema and that types match), and execution (walks the tree and calls the resolver for each field, from the outside in).
Every field in the schema has exactly one resolver. If a field doesn’t define its own resolver, GraphQL falls back to a default one that looks for a property with the same name on the parent object. This is key to understanding performance: a nested query (user, posts, comments) triggers one resolver per node in the tree, not a single database query.
The graphql-js execution engine resolves fields at the same level in parallel when their resolvers are asynchronous, so an object with ten fields hitting different sources doesn’t wait for each one to finish before starting the next. The real dependency shows up between levels: resolving Usuario.publicaciones first requires the user id returned by Query.usuario.
flowchart TD
A["Client (web, mobile)"] --> B["GraphQL Server"]
B --> C["Resolver: Query.usuario"]
C --> D["Resolver: Usuario.posts"]
D --> E["Resolver: Post.comentarios"]
B --> F[("Database / internal APIs")]
C --> F
D --> F
E --> F
Practical Examples
The “hello world”: a minimal schema
This first example spins up the smallest possible GraphQL server, using plain graphql-js, with no HTTP framework.
const { graphql, buildSchema } = require('graphql');
const schema = buildSchema(`
type Query {
saludo: String
}
`);
const root = {
saludo: () => 'Hello from GraphQL',
};
graphql({ schema, source: '{ saludo }', rootValue: root }).then((respuesta) => {
console.log(respuesta);
});
Running this, the console prints { data: { saludo: ‘Hello from GraphQL’ } }. The query { saludo } asks the server for exactly one field, and the root.saludo resolver returns the string.
A realistic schema: users and posts
This second example adds related types, typical of a blog or social network, plus a mutation to write data.
const schema = buildSchema(`
type Usuario {
id: ID!
nombre: String!
publicaciones: [Publicacion!]!
}
type Publicacion {
id: ID!
titulo: String!
autor: Usuario!
}
type Query {
usuario(id: ID!): Usuario
}
type Mutation {
crearPublicacion(usuarioId: ID!, titulo: String!): Publicacion
}
`);
const usuarios = { '1': { id: '1', nombre: 'Marta' } };
const publicaciones = [];
const root = {
usuario: ({ id }) => ({
...usuarios[id],
publicaciones: publicaciones.filter((p) => p.autorId === id),
}),
crearPublicacion: ({ usuarioId, titulo }) => {
const nueva = { id: String(publicaciones.length + 1), titulo, autorId: usuarioId };
publicaciones.push(nueva);
return { ...nueva, autor: usuarios[usuarioId] };
},
};
With this schema, a client can request usuario(id: “1”) { nombre publicaciones { titulo } } and get back, in a single round trip, the user along with their posts, something that in REST would have required two calls (/usuarios/1 and /usuarios/1/publicaciones).
Getting Started: Setting Up a GraphQL Server with Express
To try this in a real project, with an HTTP endpoint and an interface for exploring the schema, you need express and graphql-http.
npm install express graphql-http graphql
const express = require('express');
const { createHandler } = require('graphql-http/lib/use/express');
const { buildSchema } = require('graphql');
const schema = buildSchema(`
type Query {
version: String
}
`);
const app = express();
app.all('/graphql', createHandler({ schema, rootValue: { version: () => '1.0.0' } }));
app.listen(4000, () => console.log('GraphQL at http://localhost:4000/graphql'));
With the server up, you can confirm it responds using an introspection query:
curl -X POST http://localhost:4000/graphql -H 'Content-Type: application/json' -d '{"query": "{ __schema { queryType { name } } }"}'
If the response comes back as {“data”:{“__schema”:{“queryType”:{“name”:”Query”}}}}, the server is serving the schema correctly. That same introspection mechanism is what GraphiQL and Apollo Sandbox use to autocomplete queries without external documentation.
Real-World Use Cases
GitHub rewrote a large part of its public API on top of GraphQL: the GitHub API v4 coexists with REST v3 and lets you request, in a single query, a repository along with its related issues and pull requests, something that v3 required several chained calls to do.
Shopify exposes its Storefront API in GraphQL so that stores can build exactly the catalog they need without burdening the client with internal inventory fields they’ll never display. GitLab also migrated a large part of its internal API to GraphQL for the same reason: a single client, GitLab’s own interface, consuming deeply nested data like project, pipeline, jobs, and artifacts.
Not every case calls for GraphQL. A simple public API, with few resources and heavy repeated read traffic, benefits more from REST’s native HTTP caching (Cache-Control, CDNs that cache by URL) than from GraphQL’s query flexibility, which by design breaks that URL-based caching.
Common Mistakes and Best Practices
| Problem | Why It Happens | How to Avoid It |
|---|---|---|
| N+1 problem | A nested resolver fires a database query for every element in the parent array | Batch with DataLoader: it groups the requested ids within the same cycle and runs a single query with WHERE id IN (…) |
| Malicious nested queries | A client crafts a query with deep nesting that multiplies the server’s workload | Limit maximum depth and calculate a cost per query before executing it |
| Missing pagination | An unbounded list-type field returns the entire table in a single response | Use the Relay connections pattern (edges, cursor, pageInfo) from the schema design stage |
| Broken URL-based caching | Since a single endpoint is used, traditional URL-based HTTP caching stops working | Cache per query with persisted queries or a response-caching layer |
The N+1 problem deserves a concrete example. In the users-and-posts schema above, if the Usuario.publicaciones resolver ran its own SQL query for every user returned in a list, a query requesting 50 users along with their posts would trigger 51 database queries: one for the list, and fifty for each user’s posts.
const DataLoader = require('dataloader');
const publicacionesLoader = new DataLoader(async (usuarioIds) => {
const filas = await db.query(
'SELECT * FROM publicaciones WHERE autor_id = ANY($1)',
[usuarioIds]
);
return usuarioIds.map((id) => filas.filter((f) => f.autor_id === id));
});
// in the resolver:
publicaciones: (usuario) => publicacionesLoader.load(usuario.id)
DataLoader batches all the .load() calls that happen within the same event loop cycle and resolves them with a single batched query, so the 51 queries from the previous example become 2.
⚠️ Watch out: DataLoader caches per request: if you need to invalidate a value within the same query, for example after a mutation, you have to explicitly call loader.clear(id).
Comparison with Alternatives
| Option | When to Use It | Advantage | Limitation |
|---|---|---|---|
| REST | Simple resources, straightforward CRUD, need URL-based caching | Native HTTP caching, minimal learning curve | Over-fetching and under-fetching with nested data |
| GraphQL | Clients with very different data needs (web, mobile) or deeply nested data | The client requests exactly what it needs, through a single endpoint | Traditional HTTP caching doesn’t apply, and query cost needs careful management |
| gRPC | High-performance service-to-service communication, with strict contracts via protobuf | Binary serialization, native bidirectional streaming | Not designed to be consumed directly from the browser |
Going Deeper: Persisted Queries, Federation, and Cost Control
In production, many teams don’t send the full query text from the client on every request. Instead, they register each query with a hash during a prior build step, and the client only sends that hash. This reduces the request size and lets the server reject any query that isn’t on the whitelist, closing off the malicious-query vector at the root.
When a single schema isn’t enough because multiple teams maintain separate services, federation comes in: each team exposes a subgraph with its own schema and resolvers, and a gateway composes all the subgraphs into a single schema facing the client.
flowchart TD
A["Client"] --> B["Federated Gateway"]
B --> C["Subgraph: Users"]
B --> D["Subgraph: Posts"]
B --> E["Subgraph: Payments"]
C --> F[("Users DB")]
D --> G[("Posts DB")]
E --> H[("Payments API")]
For real-time data, like a chat or a notification, GraphQL defines a third operation type in addition to query and mutation. The client subscribes to an event, and the server pushes an update every time that event occurs, typically over WebSockets using the graphql-ws protocol.
sequenceDiagram
participant C as Client
participant S as GraphQL Server
participant R as Resolvers
C->>S: sends GraphQL query
S->>S: parses the text into an AST
S->>S: validates the AST against the schema
S->>R: executes resolver for each field
R-->>S: returns the value of each field
S-->>C: responds with JSON in the requested shape
To confirm that a depth limit is active, just send a query with more nesting than allowed and check that the server responds with a validation error before ever touching the resolvers:
const depthLimit = require('graphql-depth-limit');
const { validate } = require('graphql');
const errores = validate(schema, documentoConsultaProfunda, [depthLimit(5)]);
console.log(errores.length > 0 ? 'Query rejected due to depth' : 'Query accepted');
📖 Summary on Telegram: View summary
Your next step: install graphql and graphql-http in an empty project, copy the users-and-posts schema from this article, and add a third related type, for example Comment, to practice resolving one more level of nesting.
Frequently Asked Questions
Does GraphQL replace REST?
Not necessarily. Many teams combine both: REST for simple, cacheable endpoints, and GraphQL for screens that need deeply nested data or come from clients with different requirements.
Does GraphQL always use HTTP?
The specification doesn’t mandate a specific transport, but in practice the vast majority of implementations use HTTP POST for queries and mutations, and WebSockets for subscriptions.
Do I need a special database to use GraphQL?
No. GraphQL is a query layer: resolvers can read from SQL, from another REST API, from a microservice, or from any source you already have.
How is a GraphQL schema versioned?
The common practice is not to version it at all: new fields and types are added without removing existing ones, and obsolete fields are marked with the @deprecated directive until nobody queries them anymore.
What is the N+1 problem, and do you always need DataLoader to fix it?
It’s the pattern where a nested resolver fires a query for every element in a list. DataLoader is the most common solution in Node.js, but any per-request batching and caching mechanism solves the same problem.
Is GraphQL faster than REST?
It’s not an automatic guarantee: it reduces network traffic by avoiding over-fetching, but a poorly designed query, without depth limits, can end up slower than several simple REST endpoints.
References
- GraphQL.org: the official specification, tutorials, and reference documentation for the language.
- graphql-js on GitHub: the reference implementation in JavaScript, maintained by the GraphQL Foundation.
- GitHub’s GraphQL API: documentation for GitHub’s API v4, a real production use case.
- Apollo documentation: guides on Apollo Server, Apollo Federation, and batching patterns like DataLoader.
📱 Enjoy this content? Follow @programacion on Telegram for daily tech content in Spanish: quick summaries, fresh content every day.
Imagen destacada: Foto de Gabriel Heinzer en Unsplash
0 Comments