GraphQL es un lenguaje de consulta y manipulación de datos para APIs, desarrollado por Facebook en 2012 y liberado como open source en 2015.
El esquema es el corazón de cualquier API GraphQL. Define los tipos de datos y las relaciones entre ellos.
GraphQL incluye varios tipos escalares por defecto:
Int
: Número entero con signo de 32 bits.Float
: Número de punto flotante con signo.String
: Secuencia de caracteres UTF-8.Boolean
: true
o false
.ID
: Identificador único, serializado como String.type Usuario {
id: ID!
nombre: String!
email: String!
edad: Int
posts: [Post!]! # Lista no nula de Posts no nulos
}
Útiles para pasar objetos complejos como argumentos, especialmente en mutaciones.
input UsuarioInput {
nombre: String!
email: String!
password: String!
}
Permiten definir tipos que deben implementar ciertos campos.
interface Nodo {
id: ID!
}
type Usuario implements Nodo {
id: ID!
nombre: String!
}
type Producto implements Nodo {
id: ID!
nombre: String!
precio: Float!
}
Representan un tipo que podría ser uno de varios tipos.
union ResultadoBusqueda = Usuario | Producto | Categoria
Las consultas permiten obtener datos de la API GraphQL.
query {
usuario(id: "123") {
nombre
email
}
}
query {
usuarios(limite: 10, ordenarPor: "nombre") {
id
nombre
}
}
Permiten renombrar campos en la respuesta.
query {
primerUsuario: usuario(id: "1") {
nombre
}
segundoUsuario: usuario(id: "2") {
nombre
}
}
Reutilizan conjuntos de campos.
fragment datosUsuario on Usuario {
id
nombre
email
}
query {
usuario(id: "123") {
...datosUsuario
edad
}
}
Hacen las consultas más dinámicas y reutilizables.
query ObtenerUsuario($userId: ID!) {
usuario(id: $userId) {
nombre
email
}
}
Variables enviadas:
{
"userId": "123"
}
Modifican la ejecución de la consulta.
query ($incluirEmail: Boolean!) {
usuario(id: "123") {
nombre
email @include(if: $incluirEmail)
}
}
Directivas comunes:
@include(if: Boolean)
: Incluye el campo si es verdadero.@skip(if: Boolean)
: Omite el campo si es verdadero.@deprecated(reason: String)
: Marca un campo como obsoleto.Las mutaciones permiten modificar datos en el servidor.
mutation {
crearUsuario(nombre: "Juan", email: "juan@example.com") {
id
nombre
}
}
mutation CrearUsuario($input: UsuarioInput!) {
crearUsuario(input: $input) {
id
nombre
email
}
}
Variables enviadas:
{
"input": {
"nombre": "Juan",
"email": "juan@example.com",
"password": "secreto"
}
}
mutation {
crear: crearUsuario(input: {nombre: "Ana"}) {
id
}
actualizar: actualizarUsuario(id: "123", cambios: {edad: 30}) {
id
}
}
Técnica para mejorar la experiencia de usuario:
Permiten recibir actualizaciones en tiempo real del servidor.
subscription {
mensajeNuevo(salaId: "123") {
id
texto
usuario {
nombre
}
}
}
Las suscripciones generalmente usan WebSockets:
// Servidor
const { ApolloServer, PubSub } = require('apollo-server');
const pubsub = new PubSub();
const typeDefs = gql`
type Subscription {
mensajeNuevo: Mensaje
}
`;
const resolvers = {
Subscription: {
mensajeNuevo: {
subscribe: () => pubsub.asyncIterator(['MENSAJE_NUEVO']),
},
},
};
// Cuando ocurre un evento:
pubsub.publish('MENSAJE_NUEVO', { mensajeNuevo: nuevoMensaje });
Los resolvers son funciones que contienen la lógica para obtener los datos de cada campo.
const resolvers = {
Query: {
usuario: (parent, args, context, info) => {
// parent: Resultado del resolver padre
// args: Argumentos de la consulta
// context: Objeto compartido entre todos los resolvers
// info: Información sobre la consulta actual
return db.usuarios.find(u => u.id === args.id);
},
},
Usuario: {
posts: (usuario) => {
return db.posts.filter(p => p.autorId === usuario.id);
},
},
};
El contexto es un objeto compartido entre todos los resolvers:
const server = new ApolloServer({
typeDefs,
resolvers,
context: ({ req }) => {
// Obtener el token del header
const token = req.headers.authorization || '';
// Intentar obtener un usuario con el token
const usuario = obtenerUsuario(token);
// Agregar el usuario al contexto
return { usuario, db };
},
});
Patrón para optimizar y batch requests a la base de datos:
const DataLoader = require('dataloader');
// Crear un loader para usuarios
const usuarioLoader = new DataLoader(ids => {
return db.usuarios.find({ id: { $in: ids } });
});
// En el resolver
Usuario: {
posts: (usuario, args, { postLoader }) => {
return postLoader.load(usuario.id);
},
},
Para aplicar lógica común a múltiples resolvers:
function autenticado(next) {
return (parent, args, context, info) => {
if (!context.usuario) {
throw new Error('No autenticado');
}
return next(parent, args, context, info);
};
}
const resolvers = {
Query: {
datosSensibles: autenticado((parent, args, context) => {
// Lógica del resolver
}),
},
};
{
"errors": [
{
"message": "No autenticado",
"locations": [ { "line": 2, "column": 3 } ],
"path": [ "datosSensibles" ],
"extensions": {
"code": "UNAUTHENTICATED",
"timestamp": "2023-01-01T12:00:00Z"
}
}
],
"data": null
}
class ErrorValidacion extends Error {
constructor(message, code, propiedades) {
super(message);
this.extensions = { code, ...propiedades };
}
}
// En el resolver
if (!context.usuario) {
throw new ErrorValidacion('No autenticado', 'UNAUTHENTICATED', {
sugerencia: 'Por favor inicia sesión',
});
}
// Apollo Client
const { error } = await client.mutate({
mutation: LOGIN,
variables: { input },
onError: (error) => {
if (error.graphQLErrors.some(e => e.extensions.code === 'UNAUTHENTICATED')) {
// Manejar error de autenticación
}
},
});
usuarios(limite: 10, offset: 20)
usuarios(primero: 10, despues: "cursor")
type Query {
usuarios(primero: Int, despues: String): UsuarioConnection!
}
type UsuarioConnection {
edges: [UsuarioEdge!]!
pageInfo: PageInfo!
}
type UsuarioEdge {
node: Usuario!
cursor: String!
}
type PageInfo {
hasNextPage: Boolean!
endCursor: String
}
query {
usuarios(primero: 10) {
edges {
node {
id
nombre
}
cursor
}
pageInfo {
hasNextPage
endCursor
}
}
}
usuarios: (_, { primero, despues }) => {
const limite = primero || 10;
const cursor = despues ? decodificarCursor(despues) : null;
const usuarios = db.usuarios
.filter(u => cursor ? u.createdAt < cursor : true)
.slice(0, limite + 1); // +1 para saber si hay más páginas
const hasNextPage = usuarios.length > limite;
const nodos = hasNextPage ? usuarios.slice(0, -1) : usuarios;
return {
edges: nodos.map(usuario => ({
node: usuario,
cursor: codificarCursor(usuario.createdAt),
})),
pageInfo: {
hasNextPage,
endCursor: nodos.length > 0 ?
codificarCursor(nodos[nodos.length - 1].createdAt) : null,
},
};
},
Autenticación: Verificar quién es el usuario.
Autorización: Verificar qué puede hacer el usuario.
// Middleware de autenticación
const context = ({ req }) => {
const token = req.headers.authorization || '';
try {
const usuario = jwt.verify(token.replace('Bearer ', ''), SECRETO);
return { usuario, db };
} catch (e) {
return { db };
}
};
// Directiva @auth
const { SchemaDirectiveVisitor } = require('apollo-server');
class AuthDirective extends SchemaDirectiveVisitor {
visitFieldDefinition(field) {
const { resolve = defaultFieldResolver } = field;
field.resolve = async function (...args) {
const [, , context] = args;
if (!context.usuario) {
throw new Error('No autenticado');
}
return resolve.apply(this, args);
};
}
}
Proteger contra consultas maliciosas o muy complejas.
const depthLimit = require('graphql-depth-limit');
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [depthLimit(5)], // Limitar a 5 niveles de profundidad
});
app.use('/graphql', bodyParser.json({ limit: '100kb' }));
const { createComplexityLimitRule } = require('graphql-validation-complexity');
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [
createComplexityLimitRule(1000, {
onCost: (cost) => console.log('Costo de consulta:', cost),
}),
],
});
extend type Query {
buscar(texto: String!): [ResultadoBusqueda!]!
}
extend type Mutation {
cambiarPassword(input: CambiarPasswordInput!): Boolean!
}
GraphQL evita el versionamiento mediante:
@deprecated
."""
Un usuario en nuestro sistema
"""
type Usuario {
"""
El ID único del usuario
"""
id: ID!
"""
El nombre completo del usuario
"""
nombre: String!
"""
La dirección de email del usuario
"""
email: String! @deprecated(reason: "Usar 'username' en su lugar")
}
type Query {
usuarios(limite: Int = 10, offset: Int = 0): [Usuario!]!
usuario(id: ID!): Usuario
posts(limite: Int = 10, offset: Int = 0): [Post!]!
post(id: ID!): Post
buscar(texto: String!): [ResultadoBusqueda!]!
}
type Mutation {
crearUsuario(input: UsuarioInput!): Usuario!
actualizarUsuario(id: ID!, input: UsuarioInput!): Usuario!
eliminarUsuario(id: ID!): Boolean!
crearPost(input: PostInput!): Post!
comentar(postId: ID!, texto: String!): Comentario!
}
type Subscription {
nuevoComentario(postId: ID!): Comentario!
}
input UsuarioInput {
nombre: String!
email: String!
password: String!
}
input PostInput {
titulo: String!
contenido: String!
autorId: ID!
}
type Usuario {
id: ID!
nombre: String!
email: String!
posts: [Post!]!
}
type Post {
id: ID!
titulo: String!
contenido: String!
autor: Usuario!
comentarios: [Comentario!]!
fechaCreacion: String!
}
type Comentario {
id: ID!
texto: String!
autor: Usuario!
post: Post!
fechaCreacion: String!
}
union ResultadoBusqueda = Usuario | Post | Comentario
const resolvers = {
Query: {
usuarios: (_, { limite, offset }) =>
db.usuarios.slice(offset, offset + limite),
usuario: (_, { id }) => db.usuarios.find(u => u.id === id),
posts: (_, { limite, offset }) =>
db.posts.slice(offset, offset + limite),
post: (_, { id }) => db.posts.find(p => p.id === id),
buscar: (_, { texto }) => {
const resultados = [];
// Buscar en usuarios
resultados.push(
...db.usuarios
.filter(u => u.nombre.includes(texto) || u.email.includes(texto))
.map(u => ({ __typename: 'Usuario', ...u }))
);
// Buscar en posts
resultados.push(
...db.posts
.filter(p => p.titulo.includes(texto) || p.contenido.includes(texto))
.map(p => ({ __typename: 'Post', ...p }))
);
// Buscar en comentarios
resultados.push(
...db.comentarios
.filter(c => c.texto.includes(texto))
.map(c => ({ __typename: 'Comentario', ...c }))
);
return resultados;
},
},
Mutation: {
crearUsuario: (_, { input }) => {
const usuario = { id: uuid(), ...input };
db.usuarios.push(usuario);
return usuario;
},
crearPost: (_, { input }) => {
const post = {
id: uuid(),
fechaCreacion: new Date().toISOString(),
...input
};
db.posts.push(post);
return post;
},
comentar: (_, { postId, texto }, { usuario }) => {
if (!usuario) throw new Error('No autenticado');
const comentario = {
id: uuid(),
texto,
autorId: usuario.id,
postId,
fechaCreacion: new Date().toISOString(),
};
db.comentarios.push(comentario);
pubsub.publish('NUEVO_COMENTARIO', {
nuevoComentario: comentario,
postId,
});
return comentario;
},
},
Subscription: {
nuevoComentario: {
subscribe: (_, { postId }, { pubsub }) => {
return pubsub.asyncIterator(['NUEVO_COMENTARIO']);
},
resolve: (payload, args) => {
if (payload.postId === args.postId) {
return payload.nuevoComentario;
}
},
},
},
Usuario: {
posts: (usuario) => db.posts.filter(p => p.autorId === usuario.id),
},
Post: {
autor: (post) => db.usuarios.find(u => u.id === post.autorId),
comentarios: (post) => db.comentarios.filter(c => c.postId === post.id),
},
Comentario: {
autor: (comentario) => db.usuarios.find(u => u.id === comentario.autorId),
post: (comentario) => db.posts.find(p => p.id === comentario.postId),
},
ResultadoBusqueda: {
__resolveType: (obj) => obj.__typename,
},
};
GraphQL es una tecnología poderosa que ofrece ventajas significativas sobre REST en muchos escenarios. Al dominar los conceptos presentados en este manual, estarás preparado para diseñar e implementar APIs GraphQL eficientes, flexibles y fáciles de usar.