⏱️ 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
  1. TL;DR
  2. What GraphQL Is and Why It Matters
  3. How GraphQL Works in Detail
  4. Practical Examples
    1. The “hello world”: a minimal schema
    2. A realistic schema: users and posts
  5. Getting Started: Setting Up a GraphQL Server with Express
  6. Real-World Use Cases
  7. Common Mistakes and Best Practices
  8. Comparison with Alternatives
  9. Going Deeper: Persisted Queries, Federation, and Cost Control
  10. Frequently Asked Questions
    1. Does GraphQL replace REST?
    2. Does GraphQL always use HTTP?
    3. Do I need a special database to use GraphQL?
    4. How is a GraphQL schema versioned?
    5. What is the N+1 problem, and do you always need DataLoader to fix it?
    6. Is GraphQL faster than REST?
  11. 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.
Diagram of a connected data graph representing a GraphQL schema
Facebook used GraphQL internally starting in 2012, before open-sourcing it in 2015. Foto de Ilya Pavlov en Unsplash

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.

Code of a GraphQL resolver on a screen
DataLoader batches repeated queries into a single request per event loop cycle. Foto de Mohammad Rahmani en Unsplash

Common Mistakes and Best Practices

ProblemWhy It HappensHow to Avoid It
N+1 problemA nested resolver fires a database query for every element in the parent arrayBatch with DataLoader: it groups the requested ids within the same cycle and runs a single query with WHERE id IN (…)
Malicious nested queriesA client crafts a query with deep nesting that multiplies the server’s workloadLimit maximum depth and calculate a cost per query before executing it
Missing paginationAn unbounded list-type field returns the entire table in a single responseUse the Relay connections pattern (edges, cursor, pageInfo) from the schema design stage
Broken URL-based cachingSince a single endpoint is used, traditional URL-based HTTP caching stops workingCache 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

OptionWhen to Use ItAdvantageLimitation
RESTSimple resources, straightforward CRUD, need URL-based cachingNative HTTP caching, minimal learning curveOver-fetching and under-fetching with nested data
GraphQLClients with very different data needs (web, mobile) or deeply nested dataThe client requests exactly what it needs, through a single endpointTraditional HTTP caching doesn’t apply, and query cost needs careful management
gRPCHigh-performance service-to-service communication, with strict contracts via protobufBinary serialization, native bidirectional streamingNot 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


Andrés Morales

Developer and AI researcher. Writes about language models, frameworks, developer tooling, and open source releases. Covers ML papers, the tech startup ecosystem, and programming trends.

0 Comments

Leave a Reply

Avatar placeholder

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.