TechLead
Intermedio
35 min
Lección 4 de 10
API

GraphQL

Aprende consultas, mutaciones y suscripciones de GraphQL para obtención flexible de datos

¿Qué es GraphQL?

GraphQL es un lenguaje de consulta y runtime para APIs desarrollado por Facebook en 2012 y de código abierto en 2015. A diferencia de REST donde el servidor define qué datos se devuelven, GraphQL permite a los clientes solicitar exactamente los datos que necesitan, ni más ni menos.

GraphQL opera a través de un único endpoint y utiliza un sistema de tipos fuerte para describir datos, habilitando herramientas poderosas para desarrolladores y validación en tiempo de ejecución.

📊 GraphQL vs REST

REST

  • • Múltiples endpoints
  • • Sobre/sub obtención
  • • Múltiples peticiones necesarias
  • • Estructura de datos fija

GraphQL

  • • Un único endpoint
  • • Obtén exactamente lo que necesitas
  • • Una petición para todos los datos
  • • Consultas flexibles del cliente

Sintaxis Básica de Consulta

// Estructura de Consulta GraphQL
// Consulta para obtener datos de usuario
query {
  user(id: "123") {
    id
    name
    email
    posts {
      title
      createdAt
    }
  }
}

// Respuesta - coincide exactamente con la forma de la consulta
{
  "data": {
    "user": {
      "id": "123",
      "name": "Juan Pérez",
      "email": "juan@example.com",
      "posts": [
        { "title": "Hola Mundo", "createdAt": "2024-01-15" },
        { "title": "Conceptos Básicos de GraphQL", "createdAt": "2024-01-20" }
      ]
    }
  }
}

// Comparar con REST - necesitarías múltiples peticiones:
// GET /api/users/123
// GET /api/users/123/posts
// ¡Y podrías obtener campos extra que no necesitas!

Hacer Peticiones GraphQL con Fetch

// Petición GraphQL básica
async function graphqlRequest(query, variables = {}) {
  const response = await fetch('https://api.example.com/graphql', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': 'Bearer tu-token'
    },
    body: JSON.stringify({
      query,
      variables
    })
  });

  const result = await response.json();

  if (result.errors) {
    throw new Error(result.errors[0].message);
  }

  return result.data;
}

// Uso - Consulta con variables
const GET_USER = `
  query GetUser($id: ID!) {
    user(id: $id) {
      id
      name
      email
      avatar
    }
  }
`;

async function getUser(userId) {
  const data = await graphqlRequest(GET_USER, { id: userId });
  return data.user;
}

// Uso
const user = await getUser('123');
console.log(user.name); // "Juan Pérez"

Consulta con Argumentos

// Consulta con múltiples argumentos
const GET_POSTS = `
  query GetPosts($limit: Int!, $offset: Int, $status: PostStatus) {
    posts(limit: $limit, offset: $offset, status: $status) {
      id
      title
      excerpt
      author {
        name
        avatar
      }
      tags {
        name
      }
      publishedAt
    }
    postsCount(status: $status)
  }
`;

async function getPosts(page = 1, status = 'PUBLISHED') {
  const limit = 10;
  const offset = (page - 1) * limit;

  const data = await graphqlRequest(GET_POSTS, {
    limit,
    offset,
    status
  });

  return {
    posts: data.posts,
    total: data.postsCount,
    hasMore: data.postsCount > page * limit
  };
}

// Consultas anidadas - obtener datos relacionados en una petición
const GET_POST_WITH_COMMENTS = `
  query GetPost($id: ID!) {
    post(id: $id) {
      id
      title
      content
      author {
        id
        name
        bio
      }
      comments {
        id
        text
        author {
          name
        }
        createdAt
      }
    }
  }
`;

Mutaciones - Crear y Actualizar Datos

// Mutación para crear datos
const CREATE_POST = `
  mutation CreatePost($input: CreatePostInput!) {
    createPost(input: $input) {
      id
      title
      slug
      status
      createdAt
    }
  }
`;

async function createPost(postData) {
  const data = await graphqlRequest(CREATE_POST, {
    input: {
      title: postData.title,
      content: postData.content,
      tags: postData.tags
    }
  });
  
  return data.createPost;
}

// Uso
const newPost = await createPost({
  title: 'Mi Nuevo Post',
  content: 'Este es el contenido...',
  tags: ['javascript', 'graphql']
});

// Mutación para actualizar datos
const UPDATE_POST = `
  mutation UpdatePost($id: ID!, $input: UpdatePostInput!) {
    updatePost(id: $id, input: $input) {
      id
      title
      content
      updatedAt
    }
  }
`;

async function updatePost(id, updates) {
  const data = await graphqlRequest(UPDATE_POST, {
    id,
    input: updates
  });
  
  return data.updatePost;
}

// Mutación para eliminar
const DELETE_POST = `
  mutation DeletePost($id: ID!) {
    deletePost(id: $id) {
      success
      message
    }
  }
`;

async function deletePost(id) {
  const data = await graphqlRequest(DELETE_POST, { id });
  return data.deletePost.success;
}

Fragmentos - Piezas de Consulta Reutilizables

// Definir fragmentos reutilizables
const USER_FRAGMENT = `
  fragment UserFields on User {
    id
    name
    email
    avatar
    role
  }
`;

const POST_FRAGMENT = `
  fragment PostFields on Post {
    id
    title
    excerpt
    publishedAt
    author {
      ...UserFields
    }
  }
  ${USER_FRAGMENT}
`;

// Usar fragmentos en consultas
const GET_FEED = `
  query GetFeed($limit: Int!) {
    feed(limit: $limit) {
      ...PostFields
      comments(limit: 3) {
        id
        text
        author {
          ...UserFields
        }
      }
    }
  }
  ${POST_FRAGMENT}
`;

// ¡Sin fragmentos, repetirías estos campos en todos lados!
async function getFeed() {
  const data = await graphqlRequest(GET_FEED, { limit: 20 });
  return data.feed;
}

Alias - Renombrar Campos

// Usa alias cuando necesitas el mismo campo con diferentes argumentos
const GET_USER_POSTS = `
  query GetUserPosts($userId: ID!) {
    user(id: $userId) {
      name
      
      # Obtener posts publicados y borradores en una consulta
      publishedPosts: posts(status: PUBLISHED, limit: 5) {
        id
        title
      }
      
      draftPosts: posts(status: DRAFT, limit: 5) {
        id
        title
      }
      
      # Diferentes comparaciones de usuario
      followersCount: followers { count }
      followingCount: following { count }
    }
  }
`;

// Estructura de respuesta
{
  "data": {
    "user": {
      "name": "Juan",
      "publishedPosts": [
        { "id": "1", "title": "Post Publicado" }
      ],
      "draftPosts": [
        { "id": "2", "title": "Trabajo en Progreso" }
      ],
      "followersCount": { "count": 150 },
      "followingCount": { "count": 75 }
    }
  }
}

Suscripciones - Datos en Tiempo Real

// Las Suscripciones de GraphQL usan WebSocket
// Definición de suscripción
const NEW_MESSAGE = `
  subscription OnNewMessage($chatId: ID!) {
    messageAdded(chatId: $chatId) {
      id
      text
      sender {
        id
        name
      }
      createdAt
    }
  }
`;

// Usando con librería graphql-ws
import { createClient } from 'graphql-ws';

const client = createClient({
  url: 'wss://api.example.com/graphql',
  connectionParams: {
    authToken: 'tu-token'
  }
});

// Suscribirse a mensajes
function subscribeToMessages(chatId, onMessage) {
  return client.subscribe(
    {
      query: NEW_MESSAGE,
      variables: { chatId }
    },
    {
      next: (data) => {
        onMessage(data.data.messageAdded);
      },
      error: (err) => console.error('Error de suscripción:', err),
      complete: () => console.log('Suscripción completada')
    }
  );
}

// Uso
const unsubscribe = subscribeToMessages('chat-123', (message) => {
  console.log('Nuevo mensaje:', message.text);
  addMessageToUI(message);
});

// Más tarde: limpieza
unsubscribe();

Manejo de Errores

// GraphQL retorna errores junto con datos
// ¡La respuesta puede tener tanto datos como errores!
{
  "data": {
    "user": { "id": "1", "name": "Juan" },
    "posts": null  // Esto falló
  },
  "errors": [
    {
      "message": "No autorizado para ver posts",
      "path": ["posts"],
      "extensions": {
        "code": "UNAUTHORIZED"
      }
    }
  }
}

// Manejo integral de errores
async function graphqlRequest(query, variables = {}) {
  try {
    const response = await fetch('/graphql', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ query, variables })
    });

    if (!response.ok) {
      throw new Error(`Error de red: ${response.status}`);
    }

    const result = await response.json();

    // Manejar errores de GraphQL
    if (result.errors) {
      const error = result.errors[0];
      
      // Verificar tipo de error
      switch (error.extensions?.code) {
        case 'UNAUTHORIZED':
          throw new AuthError('Por favor inicia sesión');
        case 'VALIDATION_ERROR':
          throw new ValidationError(error.message, error.extensions.validationErrors);
        case 'NOT_FOUND':
          throw new NotFoundError(error.message);
        default:
          throw new GraphQLError(error.message);
      }
    }

    return result.data;
  } catch (error) {
    console.error('Petición GraphQL falló:', error);
    throw error;
  }
}

// Clases de error personalizadas
class GraphQLError extends Error {
  constructor(message) {
    super(message);
    this.name = 'GraphQLError';
  }
}

class AuthError extends GraphQLError {
  constructor(message) {
    super(message);
    this.name = 'AuthError';
  }
}

Clase de Cliente GraphQL

// Cliente GraphQL reutilizable
class GraphQLClient {
  constructor(endpoint, options = {}) {
    this.endpoint = endpoint;
    this.headers = {
      'Content-Type': 'application/json',
      ...options.headers
    };
  }

  setAuthToken(token) {
    this.headers['Authorization'] = `Bearer ${token}`;
  }

  async query(query, variables = {}) {
    return this.request(query, variables);
  }

  async mutate(mutation, variables = {}) {
    return this.request(mutation, variables);
  }

  async request(query, variables) {
    const response = await fetch(this.endpoint, {
      method: 'POST',
      headers: this.headers,
      body: JSON.stringify({ query, variables })
    });

    const result = await response.json();

    if (result.errors) {
      throw new Error(result.errors[0].message);
    }

    return result.data;
  }
}

// Uso
const client = new GraphQLClient('https://api.example.com/graphql');
client.setAuthToken('jwt-token');

// Consultas
const users = await client.query(`
  query { users { id name } }
`);

// Mutaciones
const newUser = await client.mutate(`
  mutation($name: String!, $email: String!) {
    createUser(name: $name, email: $email) {
      id name email
    }
  }
`, { name: 'Juan', email: 'juan@example.com' });

💡 Mejores Prácticas de GraphQL

  • Solicita solo campos necesarios - Evita sobre-obtención
  • Usa fragmentos - Consultas DRY y mantenibles
  • Nombra tus operaciones - Depuración y caché más fáciles
  • Usa variables - Nunca interpolación de strings
  • Maneja respuestas parciales - Datos y errores pueden coexistir
  • Implementa paginación - Usa paginación basada en cursor para conjuntos de datos grandes