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