Visión General
GraphQL es un lenguaje de consultas y un runtime para APIs donde el cliente declara exactamente qué campos necesita. En lugar de las respuestas fijas de REST, un único endpoint /graphql sirve combinaciones arbitrarias de campos — por eso encaja en apps cuyas pantallas consumen cortes distintos de los mismos datos.
Esta receta construye una API GraphQL funcional en tres stacks: Python con Strawberry y FastAPI, JavaScript con Apollo Server 4 y Java con Spring for GraphQL. Cada ejemplo incluye una Query y una Mutation para que veas las dos caras del contrato.
Cuándo Usar
Usa este recurso cuando:
- Tus clientes necesitan flexibilidad al pedir datos (apps móviles con ancho de banda limitado, dashboards con layouts variables).
- Quieres un contrato de API fuertemente tipado con documentación viva que sale sola del schema vía introspection.
- Necesitas agregar datos de varios microservicios — para llamadas servicio-a-servicio dentro de tu propio cluster, gRPC suele ser mejor opción.
- Los consumidores de tu API piden combinaciones de campos distintas con frecuencia.
Solución
Python
Strawberry + FastAPI es la pareja moderna: los tipos salen de decoradores y el router se monta bajo /graphql.
import strawberry
from fastapi import FastAPI
from strawberry.fastapi import GraphQLRouter
BOOKS = [
{"title": "Clean Code", "author": "Robert C. Martin", "pages": 464},
{"title": "The Pragmatic Programmer", "author": "Andy Hunt", "pages": 352},
]
@strawberry.type
class Book:
title: str
author: str
pages: int
@strawberry.type
class Query:
@strawberry.field
def books(self) -> list[Book]:
return [Book(**b) for b in BOOKS]
@strawberry.type
class Mutation:
@strawberry.mutation
def add_book(self, title: str, author: str, pages: int) -> Book:
BOOKS.append({"title": title, "author": author, "pages": pages})
return Book(title=title, author=author, pages=pages)
schema = strawberry.Schema(query=Query, mutation=Mutation)
app = FastAPI()
app.include_router(GraphQLRouter(schema), prefix="/graphql")
Ejecuta uvicorn main:app --reload y abre http://localhost:8000/graphql — Strawberry incluye GraphiQL en esa ruta.
JavaScript
Apollo Server 4 separa el transporte del núcleo: startStandaloneServer te da un servidor completo sin framework web.
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
const typeDefs = `#graphql
type Book {
title: String!
author: String!
pages: Int!
}
type Query {
books: [Book!]!
}
type Mutation {
addBook(title: String!, author: String!, pages: Int!): Book!
}
`;
const books = [
{ title: 'Clean Code', author: 'Robert C. Martin', pages: 464 },
{ title: 'The Pragmatic Programmer', author: 'Andy Hunt', pages: 352 },
];
const resolvers = {
Query: {
books: () => books,
},
Mutation: {
addBook: (_, { title, author, pages }) => {
const book = { title, author, pages };
books.push(book);
return book;
},
},
};
const server = new ApolloServer({ typeDefs, resolvers });
const { url } = await startStandaloneServer(server, {
listen: { port: 4000 },
});
console.log(`Servidor listo en ${url}`);
Guárdalo como index.mjs (el await de nivel superior requiere ESM) y ejecútalo con node index.mjs.
Java
Spring for GraphQL es el estándar actual — el schema vive en src/main/resources/graphql/schema.graphqls y los resolvers son métodos de un controlador:
// src/main/resources/graphql/schema.graphqls
// type Book { title: String!, author: String!, pages: Int! }
// type Query { books: [Book!]! }
// type Mutation { addBook(title: String!, author: String!, pages: Int!): Book! }
@Controller
public class BookController {
private final List<Book> books = new ArrayList<>(List.of(
new Book("Clean Code", "Robert C. Martin", 464),
new Book("The Pragmatic Programmer", "Andy Hunt", 352)
));
@QueryMapping
public List<Book> books() {
return books;
}
@MutationMapping
public Book addBook(@Argument String title,
@Argument String author,
@Argument int pages) {
Book book = new Book(title, author, pages);
books.add(book);
return book;
}
}
record Book(String title, String author, int pages) {}
Añade spring-boot-starter-graphql al proyecto; el endpoint queda en /graphql por defecto. Evita las librerías antiguas graphql-java-servlet / com.coxautodev — están sin mantenimiento y todavía arrastran javax.servlet.
Suscripciones
Las suscripciones van por un transporte WebSocket, pero la forma es la misma en todas partes: un stream asíncrono que emite valores a medida que ocurren los eventos. En Strawberry una suscripción es solo un generador asíncrono:
@strawberry.type
class Subscription:
@strawberry.subscription
async def book_added(self) -> AsyncGenerator[Book, None]:
while True:
yield await BOOK_EVENTS.get() # asyncio.Queue alimentada por add_book
En JavaScript la convención es graphql-ws bajo el protocolo WS; Spring conecta métodos @SubscriptionMapping a su endpoint WebSocket de la misma manera. Si no necesitas push en vivo, sáltalo — sondear una query suele ser suficiente.
Explicación
Por dentro, una API GraphQL son tres piezas:
- Schema: el contrato tipado — tipos, queries, mutaciones, suscripciones — escrito en SDL (el archivo
typeDefs/schema.graphqls) o generado desde código. - Resolvers: una función por campo que devuelve los datos reales. Reciben
(parent, args, context, info); mantenlos sin estado. - Servidor: acepta la petición HTTP, parsea la query, la valida contra el schema, ejecuta los resolvers y ensambla
data+errors.
Vale la pena tener el ciclo de vida de la petición en la cabeza:
Dos detalles que el diagrama explica:
- Un 200 puede ser un fallo. Los errores de validación y ejecución vuelven dentro de
errorscon HTTP 200 — revisa siempre ese array del lado del cliente (más patrones en manejo de errores GraphQL). - Los resolvers corren por campo, no por petición. Una query que toca
books { author { books { ... } } }llama a un resolver por libro — ahí vive el problema N+1 y por eso existe el batching con DataLoader.
Diferencias entre lenguajes que conviene conocer:
- Python (Strawberry): code-first — los decoradores generan el SDL desde tus clases.
- JavaScript (Apollo): schema-first — SDL en
typeDefs, resolvers como objeto plano. - Java (Spring): schema-first — archivo
.graphqlsmás métodos@QueryMapping/@MutationMapping.
Variantes
| Tecnología | Biblioteca | Enfoque | Notas |
|---|---|---|---|
| Python | Strawberry | Code-first | Decoradores, integración con FastAPI |
| Python | Graphene | Code-first | Integración Django, ecosistema maduro |
| JavaScript | Apollo Server | Schema-first | Federación, suscripciones, el más usado |
| JavaScript | Nexus | Code-first | TypeScript-first, inferencia de tipos |
| Java | Spring for GraphQL | Schema-first | Starter de Spring Boot, estándar actual |
| Java | DGS Framework | Code-first | Open-source de Netflix, integración Spring |
Cuándo No Usar Este Enfoque
- CRUD simple con schema estable: para 5-10 endpoints con formas predecibles, REST es más simple, más fácil de depurar y cachea gratis.
- APIs públicas cacheadas en CDN: los POST de GraphQL esquivan la caché de borde por defecto. Para datos públicos de lectura intensiva, los GET de REST más un CDN rinden 10-100x más barato.
- Clientes con ancho de banda limitado: la introspection del schema y los payloads más grandes añaden overhead real en redes 2G/3G o enlaces IoT.
Consideraciones de Seguridad
La flexibilidad de GraphQL también es su superficie de ataque. Estos son los controles que importan:
- Límite de profundidad de queries: queries anidadas como
user { friends { friends { ... } } }agotan la memoria. Limita la profundidad a 7-10 niveles congraphql-depth-limito el equivalente de tu framework. - Análisis de complejidad de queries: asigna un coste por campo y rechaza queries que superen un presupuesto — una sola query de cliente puede desencadenar 5-15 llamadas internas en un esquema federado.
- Ataques de batch queries: varias operaciones en un solo POST esquivan los rate limits por petición. Limita el conteo del batch (5 es razonable) y aplica rate limit por operación.
- Introspection en producción: desactívala (
introspection: false) o protégela con auth — le da al atacante el mapa completo de tu superficie de API. - Persisted queries: en producción, sirve solo queries aprobadas del lado del servidor por hash. Elimina por completo la ejecución de queries arbitrarias.
- Los errores no deben filtrar internals: quita stack traces y detalle de
extensionsen los builds de producción. - Mutations y CSRF: si el endpoint acepta cookies, exige un header personalizado o pasa las mutations a auth por token.
Mejores Prácticas
- Usa DataLoader para las queries N+1: agrupa y deduplica las llamadas a BD por petición — convierte
1 + Nqueries en 2. La receta de batching cubre la mecánica. - Pagina las colecciones: usa paginación por cursores (spec Relay Connection) para listas más allá de una página.
- Valida la entrada temprano: escalares personalizados y directivas atrapan entrada mala antes de que corran los resolvers.
- Depreca, no versiones: evoluciona el schema con campos
@deprecateden lugar de endpoints/v2. - Mantén los resolvers delgados: la lógica de negocio va en una capa de servicio; los resolvers solo adaptan el plumbing de HTTP/GraphQL a ella.
- No filtres modelos de BD: mapea objetos de dominio a tipos del schema en lugar de exponer entidades del ORM directamente.
Errores Comunes
- Ignorar el array
errors: los clientes que solo revisandatadescartan fallos en silencio — GraphQL devuelve 200 conerrors: [...]tanto para fallos de validación como de resolvers. - Sin límites de profundidad o complejidad: una query hostil puede agotar el servidor. Configura
maxDepthymaxComplexityen las reglas de validación desde el día uno. - Resolvers con N+1: obtener objetos relacionados fila a fila en lugar de por lotes — una query de 100 usuarios con sus posts se convierte en 101 accesos a BD.
- Guardar estado en resolvers: los resolvers se comparten entre peticiones; los datos del scope de la petición van en
context. - Devolver errores con stack traces: los
extensionsde error en producción deben ocultar internals. - Saltarse la capa de dominio: conectar entidades del ORM directamente al schema acopla tu contrato de API a tus tablas.
Monitoring y Observabilidad
- Rastrea la latencia por resolver: p50/p95/p99 por campo — los resolvers lentos (p95 >100ms) suelen ser el cuello de botella, no la capa HTTP.
- Registra los scores de complejidad de query: alerta cuando el promedio semanal suba >20%; normalmente significa que un cliente empezó a pedir grafos más profundos.
- Vigila el ratio de lotes: los batches de DataLoader vs. llamadas totales por debajo de ~80% señalizan que el N+1 vuelve a colarse.
- Limita las conexiones de suscripción: las suscripciones WebSocket retienen memoria por cliente; alerta antes de tocar límites de FD o heap.
Estrategia de Testing
- Resolvers en aislamiento: llámalos con un
contexty argumentos de prueba; verifica valores de retorno y caminos de error. - Tests de introspection del schema: verifica que el schema desplegado expone solo los campos previstos — atrapa exposición accidental antes de que la encuentren los clientes.
- Tests de límites de complejidad: dispara queries profundamente anidadas y con muchos campos; verifica que el analizador las rechaza con un error claro.
- Ciclo de vida de suscripciones: conecta, recibe actualizaciones, desconecta, confirma que el servidor libera la suscripción.
Notas de Rendimiento y Coste
GraphQL añade overhead de parsing + validación + resolución por campo sobre REST — típicamente 2-5x de latencia para lecturas equivalentes sin caché. Las palancas que cierran la brecha, en orden de esfuerzo:
- Persisted queries — saltan el parsing por completo en tráfico repetido.
- Caché de respuestas — cachea por hash de query en Redis para endpoints de lectura intensiva (un TTL de 60s es un arranque razonable).
- DataLoader — colapsa llamadas a BD por campo en lotes.
- Caché en el gateway federado — cachea resultados de sub-resolvers en el gateway para evitar llamadas duplicadas a subgrafos.
En costes, la vía self-hosted es barata: Apollo Server o Strawberry corren en instancias comunes, y los tiers gestionados (Apollo Cloud, Hasura Cloud) sobre todo te compran el registry de schema y la caché gestionada, más que throughput bruto.
Checklist de Producción
- Límite de profundidad (7-10) y presupuesto de complejidad aplicados en las reglas de validación
- Introspection deshabilitada o protegida en producción
- Persisted queries aplicadas; queries arbitrarias rechazadas
- Batching con DataLoader en todo resolver que toca BD
- Rate limiting por operación y por conteo de batch, no solo por petición
- Las respuestas de error eliminan stack traces e
extensionsinternas - Paginación (por cursores) en todos los campos de colección
- Límites de conexión de suscripción y timeouts de heartbeat configurados
- Los cambios de schema se revisan por campos que rompen antes del despliegue
- Schema registrado en Apollo Studio o registry equivalente
Troubleshooting
- El cliente recibe data pero
errorsno está vacío: fallo parcial — algunos resolvers fallaron mientras otros funcionaron. Leeerrors[].pathpara encontrar el campo roto; revisa los logs de ese resolver, no el status HTTP. - “
Cannot query field” en producción pero funciona en local: la introspection está desactivada pero el cliente depende de la descarga del schema, o la query usa un campo que no existe en el schema desplegado — regenera los tipos del cliente desde el registry. - La memoria sube con queries anidadas: límites de profundidad/complejidad ausentes o demasiado generosos. Perfil el tamaño del plan de ejecución de la query problemática.
- Respuestas lentas pese a BD rápida: resolvers esperando en serie por fila — eso es N+1; añade DataLoader o un lote
findByIds. - Errores CORS desde el navegador: el endpoint GraphQL necesita
Access-Control-Allow-Originen el preflight como cualquier ruta de API — ver manejo de CORS.
Véase También
- Companion ejecutable — los servidores Strawberry, Apollo y Spring más queries de ejemplo.
- Sección learn oficial de GraphQL — el tutorial del propio spec.
- Documentación de Apollo Server — la API actual de
@apollo/server. - Docs de Strawberry GraphQL — decoradores, escalares e integraciones.
- Referencia de Spring for GraphQL —
@QueryMapping, testing y comparación con DGS. - OWASP GraphQL Security Cheat Sheet — límites de profundidad, ataques de batching, hardening de introspection.
- Spec de multipart request de GraphQL — la convención de subida que implementan la mayoría de servidores.
Preguntas frecuentes
¿Debería migrar mi API REST a GraphQL?
No necesariamente. GraphQL compensa cuando los clientes necesitan flexibilidad — conjuntos de campos variables, anidación profunda, un solo round-trip para una pantalla compleja. Si tus consumidores son pocos y estables, REST sigue siendo más simple y barato de cachear.
¿Cómo manejo la subida de archivos en GraphQL?
Usa el spec de GraphQL multipart request (Apollo Server lo soporta; Strawberry vía el escalar Upload) o mantén un endpoint REST pequeño para las subidas y devuelve la URL por GraphQL. La mayoría de equipos toma la segunda vía y saca las subidas del grafo.
¿Qué es la federación de GraphQL?
La federación permite que varios servicios GraphQL posean cada uno parte de un schema unificado, unido por un gateway. Es cómo los equipos de microservicios exponen un solo grafo — ver la receta de gateway de federación.
¿Por qué GraphQL devuelve 200 aunque falle?
En GraphQL el error es contenido, no transporte. Los fallos de validación, de resolvers y parciales vuelven todos como HTTP 200 con un array errors — es por diseño del spec para que el data parcial pueda entregarse igualmente. Trata errors como tu status real.
¿Cómo detengo las queries costosas?
Tres capas: configura maxDepth (7-10) para que las queries no puedan anidarse infinitamente, añade análisis de complejidad para que cada campo lleve un coste con un presupuesto total, y aplica persisted queries en producción para que solo corran queries preaprobadas.
¿Cuándo GraphQL es la elección equivocada?
Cuando la flexibilidad no se necesita: un puñado de endpoints CRUD estables, APIs públicas que viven del cacheo de borde del CDN, o clientes en redes limitadas. En esos casos la simplicidad y cacheabilidad de REST ganan directamente.
Recursos Relacionados
Batch y cache de consultas con GraphQL DataLoader
Usa DataLoader para agrupar peticiones de carga individuales en llamadas batch a la base de datos, resolviendo el problema N+1 en resolvers GraphQL
RecipeErrores GraphQL estructurados con codigos de extension
Implementa manejo de errores estructurado en GraphQL con clases de error personalizadas, codigos de extension y formato consistente para clientes
RecipeConfigurar un gateway de GraphQL Federation con Apollo
Compone multiples servicios GraphQL en un supergrafo federado usando Apollo Federation y un gateway que enruta consultas entre subgrafos
RecipeTipos escalares personalizados en GraphQL para fechas,
Define escalares GraphQL personalizados para Date, Email, URL y JSON con logica de serializacion, parseo y validacion
GuideGraphQL vs REST — Cuándo Elegir y Cómo Migrar
Guía de decisión comparando GraphQL y REST APIs: casos de uso, rendimiento, caching, herramientas y estrategias de migración para equipos de ingeniería.
GuideDiseño de Esquemas GraphQL
Disenar esquemas GraphQL para evolucion, rendimiento y mantenibilidad. Cubre diseno de tipos, conexiones, mutaciones, manejo de errores, deprecation y workflows schema-first vs code-first.