Manual Completo de GraphQL

1. Introducción a GraphQL

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.

1.1 Ventajas sobre REST

1.2 Componentes principales

2. Esquema (Schema) y Tipos

El esquema es el corazón de cualquier API GraphQL. Define los tipos de datos y las relaciones entre ellos.

2.1 Tipos escalares

GraphQL incluye varios tipos escalares por defecto:

2.2 Definición de tipos de objeto

type Usuario {
    id: ID!
    nombre: String!
    email: String!
    edad: Int
    posts: [Post!]!  # Lista no nula de Posts no nulos
}

2.3 Tipos de entrada (Input Types)

Útiles para pasar objetos complejos como argumentos, especialmente en mutaciones.

input UsuarioInput {
    nombre: String!
    email: String!
    password: String!
}

2.4 Interfaces

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!
}

2.5 Uniones

Representan un tipo que podría ser uno de varios tipos.

union ResultadoBusqueda = Usuario | Producto | Categoria

3. Consultas (Queries)

Las consultas permiten obtener datos de la API GraphQL.

3.1 Consulta básica

query {
    usuario(id: "123") {
        nombre
        email
    }
}

3.2 Consultas con argumentos

query {
    usuarios(limite: 10, ordenarPor: "nombre") {
        id
        nombre
    }
}

3.3 Aliases

Permiten renombrar campos en la respuesta.

query {
    primerUsuario: usuario(id: "1") {
        nombre
    }
    segundoUsuario: usuario(id: "2") {
        nombre
    }
}

3.4 Fragmentos

Reutilizan conjuntos de campos.

fragment datosUsuario on Usuario {
    id
    nombre
    email
}

query {
    usuario(id: "123") {
        ...datosUsuario
        edad
    }
}

3.5 Variables

Hacen las consultas más dinámicas y reutilizables.

query ObtenerUsuario($userId: ID!) {
    usuario(id: $userId) {
        nombre
        email
    }
}

Variables enviadas:

{
    "userId": "123"
}

3.6 Directivas

Modifican la ejecución de la consulta.

query ($incluirEmail: Boolean!) {
    usuario(id: "123") {
        nombre
        email @include(if: $incluirEmail)
    }
}

Directivas comunes:

4. Mutaciones (Mutations)

Las mutaciones permiten modificar datos en el servidor.

4.1 Mutación básica

mutation {
    crearUsuario(nombre: "Juan", email: "juan@example.com") {
        id
        nombre
    }
}

4.2 Mutación con variables

mutation CrearUsuario($input: UsuarioInput!) {
    crearUsuario(input: $input) {
        id
        nombre
        email
    }
}

Variables enviadas:

{
    "input": {
        "nombre": "Juan",
        "email": "juan@example.com",
        "password": "secreto"
    }
}

4.3 Mutaciones múltiples

mutation {
    crear: crearUsuario(input: {nombre: "Ana"}) {
        id
    }
    actualizar: actualizarUsuario(id: "123", cambios: {edad: 30}) {
        id
    }
}

4.4 Optimistic UI

Técnica para mejorar la experiencia de usuario:

  1. Actualizar la UI inmediatamente con el resultado esperado
  2. Enviar la mutación
  3. Revertir si hay error o confirmar si es exitoso

5. Suscripciones (Subscriptions)

Permiten recibir actualizaciones en tiempo real del servidor.

5.1 Suscripción básica

subscription {
    mensajeNuevo(salaId: "123") {
        id
        texto
        usuario {
            nombre
        }
    }
}

5.2 Implementación típica

Las suscripciones generalmente usan WebSockets:

  1. Cliente se suscribe a un evento
  2. Servidor mantiene la conexión abierta
  3. Cuando ocurre el evento, el servidor envía los datos
  4. Cliente recibe los datos y actualiza la UI

5.3 Ejemplo completo con Apollo

// 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 });

6. Resolvers y Ejecución

Los resolvers son funciones que contienen la lógica para obtener los datos de cada campo.

6.1 Resolver básico

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);
        },
    },
};

6.2 Contexto (Context)

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 };
    },
});

6.3 Data Loaders

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);
    },
},

6.4 Middleware de Resolvers

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
        }),
    },
};

7. Validación y Errores

7.1 Tipos de errores

7.2 Formato de errores

{
    "errors": [
        {
            "message": "No autenticado",
            "locations": [ { "line": 2, "column": 3 } ],
            "path": [ "datosSensibles" ],
            "extensions": {
                "code": "UNAUTHENTICATED",
                "timestamp": "2023-01-01T12:00:00Z"
            }
        }
    ],
    "data": null
}

7.3 Errores personalizados

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',
    });
}

7.4 Manejo de errores en el cliente

// 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
        }
    },
});

8. Paginación

8.1 Enfoques comunes

8.2 Implementación cursor-based

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
}

8.3 Consulta de paginación

query {
    usuarios(primero: 10) {
        edges {
            node {
                id
                nombre
            }
            cursor
        }
        pageInfo {
            hasNextPage
            endCursor
        }
    }
}

8.4 Resolver de paginación

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,
        },
    };
},

9. Seguridad

9.1 Autenticación y Autorización

Autenticación: Verificar quién es el usuario.

Autorización: Verificar qué puede hacer el usuario.

Implementación con JWT

// 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);
        };
    }
}

9.2 Profundidad y Complejidad de Consultas

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
});

9.3 Limitar Tamaño de Consultas

app.use('/graphql', bodyParser.json({ limit: '100kb' }));

9.4 Tiempos de Ejecución

const { createComplexityLimitRule } = require('graphql-validation-complexity');

const server = new ApolloServer({
    typeDefs,
    resolvers,
    validationRules: [
        createComplexityLimitRule(1000, {
            onCost: (cost) => console.log('Costo de consulta:', cost),
        }),
    ],
});

10. Herramientas y Ecosistema

10.1 Servidores GraphQL

10.2 Clientes GraphQL

10.3 Herramientas de Desarrollo

10.4 Extensiones del Esquema

extend type Query {
    buscar(texto: String!): [ResultadoBusqueda!]!
}

extend type Mutation {
    cambiarPassword(input: CambiarPasswordInput!): Boolean!
}

11. Mejores Prácticas

11.1 Diseño del Esquema

11.2 Versionamiento

GraphQL evita el versionamiento mediante:

11.3 Performance

11.4 Documentación

"""
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")
}

12. Ejemplo Completo: API de Blog

12.1 Esquema

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

12.2 Resolvers

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,
    },
};

13. Conclusión

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.

Recursos Adicionales