Suscribiendo Datos en Tiempo Real con GraphQL: Implementando Subscriptions para Experiencias Dinámicas
Este tutorial te guiará paso a paso en la implementación de GraphQL Subscriptions. Aprenderás a configurar tu servidor, definir esquemas para eventos en tiempo real y consumir estos datos desde el cliente para crear experiencias de usuario dinámicas y reactivas.
🚀 Introducción a GraphQL Subscriptions: El Poder del Tiempo Real
En el mundo moderno del desarrollo web, la demanda de aplicaciones en tiempo real es cada vez mayor. Desde chats en vivo y notificaciones instantáneas hasta actualizaciones de datos en dashboards, los usuarios esperan ver los cambios reflejados al instante. Aquí es donde GraphQL Subscriptions brillan con luz propia, ofreciendo un mecanismo robusto y eficiente para comunicar datos en tiempo real entre el servidor y el cliente. A diferencia de las Queries (para obtener datos una vez) y las Mutations (para modificar datos), las Subscriptions permiten al cliente "suscribirse" a eventos específicos y recibir actualizaciones continuas cada vez que esos eventos ocurren en el servidor.
Este tutorial explorará en profundidad qué son las GraphQL Subscriptions, cómo funcionan y, lo más importante, cómo puedes implementarlas en tus propias aplicaciones para crear experiencias de usuario verdaderamente dinámicas y reactivas.
¿Por Qué Elegir Subscriptions en GraphQL?
Existen otras tecnologías para la comunicación en tiempo real, como WebSockets directamente o Server-Sent Events (SSE). Entonces, ¿por qué GraphQL Subscriptions?
- Integración con el Ecosistema GraphQL: Mantienes un único punto de acceso y un esquema unificado para tus datos, ya sean consultas, mutaciones o suscripciones. Esto simplifica el desarrollo y el mantenimiento.
- Eficiencia en el Transporte: Al igual que con las Queries y Mutations, los clientes pueden especificar exactamente qué datos necesitan recibir de la suscripción, evitando el over-fetching de datos.
- Abstracción de WebSockets: GraphQL abstrae la complejidad de la gestión de conexiones WebSocket, permitiéndote concentrarte en la lógica de negocio y los datos.
- Manejo de Estados Simplificado: Las librerías cliente de GraphQL (como Apollo Client) ofrecen una integración fluida con las suscripciones, facilitando el manejo de datos en tiempo real en tu UI.
🛠️ Fundamentos de GraphQL Subscriptions
Antes de sumergirnos en la implementación, es crucial entender los componentes clave que hacen posible las Subscriptions.
El Esquema GraphQL y el Tipo Subscription
Similar a cómo defines los tipos Query y Mutation en tu esquema GraphQL, debes definir un tipo Subscription. Este tipo raíz es el punto de entrada para todas tus suscripciones y declara los eventos a los que los clientes pueden suscribirse.
type Subscription {
nuevoMensaje(canalId: ID!): Mensaje
usuarioConectado: Usuario
productoActualizado(id: ID!): Producto
}
type Mensaje {
id: ID!
texto: String!
autor: Usuario!
canalId: ID!
}
type Usuario {
id: ID!
nombre: String!
}
type Producto {
id: ID!
nombre: String!
precio: Float!
stock: Int!
}
En el ejemplo anterior:
nuevoMensaje(canalId: ID!): Permite suscribirse a nuevos mensajes en un canal específico. El cliente recibirá un objetoMensaje.usuarioConectado: Notifica cuando un nuevo usuario se conecta, devolviendo un objetoUsuario.productoActualizado(id: ID!): Ofrece actualizaciones sobre un producto específico, retornando unProducto.
Los Resolvers de Subscriptions
Los resolvers de Subscriptions son un poco diferentes a los de Queries o Mutations. En lugar de devolver un valor directamente, devuelven un AsyncIterator o AsyncIterable. Este es un objeto especial que emitirá valores a lo largo del tiempo. Cada vez que el servidor quiera enviar un dato a un cliente suscrito, "empujará" un nuevo valor a este iterador.
La implementación de estos resolvers generalmente implica el uso de una capa de publicación/suscripción (pub/sub) para gestionar los eventos. Librerías como graphql-subscriptions o graphql-yoga ofrecen implementaciones de PubSub que facilitan esta tarea.
Mecanismo Pub/Sub (Publish/Subscribe)
El corazón de la implementación de Subscriptions es un mecanismo Pub/Sub. Este patrón desacopla los emisores de eventos (publishers) de los receptores (subscribers). Cuando un evento ocurre (por ejemplo, se guarda un nuevo mensaje en la base de datos), el servidor "publica" este evento. El sistema Pub/Sub se encarga de notificar a todos los "suscriptores" interesados en ese tipo de evento.
Existen varias implementaciones de Pub/Sub:
PubSuben memoria: Ideal para desarrollo local o aplicaciones pequeñas de un solo proceso. No escala horizontalmente.RedisPubSub: Utiliza Redis como broker de mensajes, permitiendo que múltiples instancias de tu servidor GraphQL compartan eventos y escalen horizontalmente.KafkaPubSub/GoogleCloudPubSub/AWSSNS/SQSPubSub: Soluciones empresariales para sistemas distribuidos y de alta disponibilidad.
⚙️ Configuración del Servidor GraphQL para Subscriptions
Implementar Subscriptions requiere que tu servidor GraphQL no solo maneje peticiones HTTP (para Queries y Mutations) sino también conexiones WebSocket para las Subscriptions. Usaremos Apollo Server y ws (o @graphql-ws/ws) como ejemplo, ya que son muy populares y bien documentados.
Paso 1: Inicializar el Proyecto
Primero, crea un nuevo proyecto Node.js e instala las dependencias necesarias:
mkdir graphql-subscriptions-tutorial
cd graphql-subscriptions-tutorial
npm init -y
npm install express graphql apollo-server-express graphql-subscriptions ws
Paso 2: Definir el Esquema y los Resolvers
Crearemos un esquema simple para una aplicación de chat, donde los usuarios pueden enviar mensajes y suscribirse a nuevos mensajes en un canal.
src/schema.js
const { gql } = require('apollo-server-express');
const { PubSub } = require('graphql-subscriptions');
// Definimos el objeto PubSub que usaremos para emitir eventos
const pubsub = new PubSub();
// Nombres de los eventos
const MESSAGE_ADDED = 'MESSAGE_ADDED';
const typeDefs = gql`
type Query {
mensajes(canalId: ID!): [Mensaje]
}
type Mutation {
enviarMensaje(canalId: ID!, autor: String!, texto: String!): Mensaje
}
type Subscription {
nuevoMensaje(canalId: ID!): Mensaje
}
type Mensaje {
id: ID!
canalId: ID!
texto: String!
autor: String!
fecha: String!
}
`;
// Simulamos una base de datos de mensajes
const mensajesDB = {};
const resolvers = {
Query: {
mensajes: (_, { canalId }) => {
return mensajesDB[canalId] || [];
},
},
Mutation: {
enviarMensaje: (_, { canalId, autor, texto }) => {
if (!mensajesDB[canalId]) {
mensajesDB[canalId] = [];
}
const nuevoMensaje = {
id: String(mensajesDB[canalId].length + 1),
canalId,
autor,
texto,
fecha: new Date().toISOString(),
};
mensajesDB[canalId].push(nuevoMensaje);
// Publicar el evento para los suscriptores
pubsub.publish(MESSAGE_ADDED, { nuevoMensaje: nuevoMensaje });
return nuevoMensaje;
},
},
Subscription: {
nuevoMensaje: {
subscribe: (_, { canalId }) => {
// Filtrar mensajes por canalId si es necesario, o manejarlo en el cliente
// Aquí, simplemente escuchamos todos los mensajes y el cliente filtrará
return pubsub.asyncIterator([MESSAGE_ADDED]);
},
resolve: (payload, { canalId }) => {
// El payload es el objeto que publicamos: { nuevoMensaje: ... }
// Si el cliente se suscribió a un canal específico, filtramos aquí
if (payload.nuevoMensaje.canalId === canalId) {
return payload.nuevoMensaje;
}
return null; // No enviar este mensaje si no es del canal correcto
}
},
},
};
module.exports = { typeDefs, resolvers, pubsub };
Explicación del Subscription Resolver:
- El campo
nuevoMensajeenSubscriptiontiene una propiedadsubscribeen lugar deresolvedirectamente. subscribees una función que retorna unAsyncIterator. Aquí, usamospubsub.asyncIterator([MESSAGE_ADDED])para crear un iterador que emitirá datos cada vez que un eventoMESSAGE_ADDEDsea publicado.- La función
resolvese ejecuta cada vez que elAsyncIteratoremite un valor. Es similar a un resolver de Query, donde puedes transformar o filtrar elpayloadrecibido antes de enviarlo al cliente. En nuestro caso, nos aseguramos de que el mensaje pertenezca alcanalIdal que el cliente se suscribió.
Paso 3: Configurar el Servidor Apollo
Ahora, configuraremos Apollo Server para escuchar tanto peticiones HTTP como conexiones WebSocket.
src/index.js
const express = require('express');
const { ApolloServer } = require('apollo-server-express');
const { createServer } = require('http');
const { execute, subscribe } = require('graphql');
const { SubscriptionServer } = require('subscriptions-transport-ws'); // Más antiguo pero común
// O la alternativa más moderna para graphql-ws
// const { WebSocketServer } = require('ws');
// const { useServer } = require('graphql-ws/lib/use/ws');
const { typeDefs, resolvers, pubsub } = require('./schema');
async function startApolloServer() {
const app = express();
const httpServer = createServer(app);
const server = new ApolloServer({
typeDefs,
resolvers,
// Aquí pasamos el objeto pubsub para que esté disponible en los resolvers si fuera necesario
// Aunque en este ejemplo ya lo importamos directamente en schema.js
context: ({ req, res }) => ({ pubsub }),
});
await server.start();
server.applyMiddleware({ app });
const PORT = 4000;
// Configuración para SubscriptionServer (subscriptions-transport-ws)
SubscriptionServer.create(
{
// Estos son los mismos argumentos que pasas a ApolloServer
schema: server.schema,
execute,
subscribe,
onConnect: (connectionParams, webSocket, context) => {
console.log('Cliente conectado para suscripciones');
// Puedes realizar autenticación aquí
return { /* user: 'someUser' */ };
},
onDisconnect: (webSocket, context) => {
console.log('Cliente desconectado de suscripciones');
},
},
{
server: httpServer,
path: server.graphqlPath, // El mismo path que el HTTP endpoint
}
);
/*
// Alternativa más moderna usando graphql-ws y ws
const wsServer = new WebSocketServer({
server: httpServer,
path: server.graphqlPath,
});
useServer({
schema: server.schema,
execute,
subscribe,
onConnect: async (ctx) => {
console.log('Cliente conectado para suscripciones (graphql-ws)');
// const token = ctx.connectionParams.authToken;
// if (!token) { throw new Error('Auth token missing!'); }
},
onDisconnect(ctx, code, reason) {
console.log('Cliente desconectado (graphql-ws)');
},
}, wsServer);
*/
httpServer.listen(PORT, () => {
console.log(`🚀 Servidor GraphQL listo en http://localhost:${PORT}${server.graphqlPath}`);
console.log(`🚀 Servidor de Suscripciones listo en ws://localhost:${PORT}${server.graphqlPath}`);
});
}
startApolloServer();
💡 Consumiendo Subscriptions desde el Cliente
Una vez que nuestro servidor está configurado para emitir eventos en tiempo real, necesitamos un cliente GraphQL que pueda conectarse a los WebSockets y procesar las actualizaciones. Apollo Client es una excelente opción para esto.
Paso 1: Inicializar el Cliente Apollo
Para manejar WebSockets, Apollo Client necesita un WebSocketLink además del HttpLink normal.
npm install @apollo/client graphql-ws subscriptions-transport-ws # Instala según tu elección de servidor
src/client.js (Ejemplo de cliente React, pero los principios son los mismos)
import React from 'react';
import ReactDOM from 'react-dom';
import { ApolloClient, InMemoryCache, ApolloProvider, gql, HttpLink } from '@apollo/client';
import { split, from } from '@apollo/client';
import { WebSocketLink } from '@apollo/client/link/ws'; // Para subscriptions-transport-ws
// import { GraphQLWsLink } from '@apollo/client/link/subscriptions'; // Para graphql-ws
import { getMainDefinition } from '@apollo/client/utilities';
// 1. Configurar HttpLink para Queries y Mutations
const httpLink = new HttpLink({
uri: 'http://localhost:4000/graphql'
});
// 2. Configurar WebSocketLink para Subscriptions
// Para subscriptions-transport-ws
const wsLink = new WebSocketLink({
uri: `ws://localhost:4000/graphql`,
options: {
reconnect: true,
// connectionParams: () => ({ authToken: 'your-auth-token' }),
}
});
// Para graphql-ws (si usas useServer en el servidor)
// const wsLink = new GraphQLWsLink(createClient({
// url: `ws://localhost:4000/graphql`,
// connectionParams: async () => {
// return {
// // authToken: localStorage.getItem('apollo-token'),
// };
// },
// }));
// 3. Usar `split` para enrutar operaciones a los links correctos
// Las subscriptions van al wsLink, las queries/mutations al httpLink
const splitLink = split(
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
);
},
wsLink, // Si es una suscripción, usa el WebSocketLink
httpLink, // De lo contrario, usa el HttpLink
);
const client = new ApolloClient({
link: from([splitLink]), // Asegúrate de que splitLink esté en la cadena
cache: new InMemoryCache(),
});
// --- Componente de React para la aplicación de chat --- //
const GET_MESSAGES = gql`
query GetMessages($canalId: ID!) {
mensajes(canalId: $canalId) {
id
autor
texto
fecha
}
}
`;
const SEND_MESSAGE = gql`
mutation SendMessage($canalId: ID!, $autor: String!, $texto: String!) {
enviarMensaje(canalId: $canalId, autor: $autor, texto: $texto) {
id
autor
texto
fecha
}
}
`;
const NEW_MESSAGE_SUBSCRIPTION = gql`
subscription NuevoMensaje($canalId: ID!) {
nuevoMensaje(canalId: $canalId) {
id
autor
texto
fecha
}
}
`;
function ChatApp() {
const [canalId, setCanalId] = React.useState('general');
const [autor, setAutor] = React.useState('Anónimo');
const [mensajeTexto, setMensajeTexto] = React.useState('');
const [mensajes, setMensajes] = React.useState([]);
// Cargar mensajes iniciales
React.useEffect(() => {
client.query({
query: GET_MESSAGES,
variables: { canalId }
}).then(result => {
setMensajes(result.data.mensajes);
}).catch(error => console.error("Error al cargar mensajes iniciales:", error));
}, [canalId]);
// Suscribirse a nuevos mensajes
React.useEffect(() => {
const unsubscribe = client.subscribe({
query: NEW_MESSAGE_SUBSCRIPTION,
variables: { canalId }
}).subscribe({
next({ data }) {
if (data && data.nuevoMensaje) {
setMensajes(prevMensajes => [...prevMensajes, data.nuevoMensaje]);
}
},
error(err) { console.error('Subscription error', err); },
complete() { console.log('Subscription completed'); }
});
return () => unsubscribe.unsubscribe(); // Limpiar la suscripción al desmontar
}, [canalId]);
const handleSendMessage = async (e) => {
e.preventDefault();
if (!mensajeTexto.trim()) return;
try {
await client.mutate({
mutation: SEND_MESSAGE,
variables: { canalId, autor, texto: mensajeTexto }
});
setMensajeTexto('');
} catch (error) {
console.error('Error al enviar mensaje:', error);
}
};
return (
<div style={{ maxWidth: '600px', margin: '20px auto', fontFamily: 'sans-serif' }}>
<h1>💬 Chat GraphQL en Tiempo Real</h1>
<div style={{ marginBottom: '15px' }}>
<label>Tu Nombre: </label>
<input
type="text"
value={autor}
onChange={(e) => setAutor(e.target.value)}
style={{ padding: '8px', marginRight: '10px' }}
/>
<label>Canal: </label>
<select
value={canalId}
onChange={(e) => setCanalId(e.target.value)}
style={{ padding: '8px' }}
>
<option value="general">General</option>
<option value="tecnologia">Tecnología</option>
<option value="random">Random</option>
</select>
</div>
<div style={{ border: '1px solid #ddd', height: '400px', overflowY: 'scroll', padding: '10px', marginBottom: '15px', backgroundColor: '#f9f9f9' }}>
{mensajes.length === 0 ? (
<p>No hay mensajes en este canal. ¡Sé el primero en saludar!</p>
) : (
mensajes.map((msg, index) => (
<div key={msg.id || index} style={{ marginBottom: '8px', padding: '5px', borderRadius: '5px', backgroundColor: '#eef' }}>
<strong>{msg.autor}</strong> ({new Date(msg.fecha).toLocaleTimeString()}):
<p style={{ margin: '5px 0 0 0' }}>{msg.texto}</p>
</div>
))
)}
</div>
<form onSubmit={handleSendMessage} style={{ display: 'flex' }}>
<input
type="text"
value={mensajeTexto}
onChange={(e) => setMensajeTexto(e.target.value)}
placeholder="Escribe tu mensaje..."
style={{ flexGrow: 1, padding: '10px', border: '1px solid #ccc', borderRadius: '4px' }}
/>
<button type="submit" style={{ padding: '10px 15px', marginLeft: '10px', backgroundColor: '#007bff', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer' }}>
Enviar
</button>
</form>
</div>
);
}
ReactDOM.render(
<ApolloProvider client={client}>
<ChatApp />
</ApolloProvider>,
document.getElementById('root')
);
Este código de cliente de React (que puedes ejecutar en un entorno como Create React App) demuestra cómo:
- Configurar
HttpLinkyWebSocketLink. - Usar
splitpara dirigir correctamente las operaciones GraphQL. - Realizar consultas (
useQueryoclient.query) para obtener el estado inicial de los mensajes. - Enviar mutaciones (
useMutationoclient.mutate) para enviar nuevos mensajes. - Utilizar
client.subscribepara conectarse a la suscripciónnuevoMensajey actualizar el estado local de la aplicación cada vez que llega un nuevo mensaje.
Manejo de Conexiones y Desconexiones
Es importante que tu cliente maneje la reconexión de WebSockets automáticamente. Tanto WebSocketLink como GraphQLWsLink tienen opciones para esto (reconnect: true en el primero).
También, al usar client.subscribe, la función de retorno del useEffect (en React) o el método unsubscribe() en la llamada directa, es crucial para limpiar la suscripción cuando el componente se desmonta o ya no es necesaria, evitando fugas de memoria.
📈 Escalabilidad y Consideraciones Avanzadas
Si bien la configuración básica con PubSub en memoria es útil para aprender, las aplicaciones en producción requieren una estrategia de escalabilidad más robusta.
Usando Redis para Pub/Sub Distribuido
Para escalar horizontalmente tu servidor GraphQL (ej. múltiples instancias detrás de un balanceador de carga), necesitas un mecanismo Pub/Sub que no esté acoplado a una única instancia de memoria. Redis es una opción popular y eficiente.
npm install ioredis graphql-redis-subscriptions
Luego, modifica tu src/schema.js:
// ... (imports existentes)
const { RedisPubSub } = require('graphql-redis-subscriptions');
const Redis = require('ioredis');
// Configura tus opciones de Redis
const options = {
host: 'localhost',
port: 6379,
retryStrategy: times => Math.min(times * 50, 2000) // Estrategia de reintento
};
// Crea dos clientes Redis: uno para publicar y otro para suscribirse
const pub = new Redis(options);
const sub = new Redis(options);
// Inicializa RedisPubSub
const pubsub = new RedisPubSub({ publisher: pub, subscriber: sub });
// ... (rest of your schema.js remains the same, using the new pubsub object)
Con RedisPubSub, cuando un mensaje se publica en una instancia de tu servidor GraphQL, Redis lo retransmite a todas las otras instancias conectadas a él, que a su vez lo empujan a sus clientes suscritos. Esto garantiza que todos los clientes reciban las actualizaciones, independientemente de a qué instancia de servidor estén conectados.
Autenticación y Autorización en Subscriptions
Es fundamental asegurar tus suscripciones. Los onConnect callbacks tanto en subscriptions-transport-ws como en graphql-ws son el lugar ideal para implementar la lógica de autenticación y autorización.
En onConnect, puedes acceder a los parámetros de conexión enviados por el cliente (por ejemplo, un token JWT) y validar si el cliente tiene permiso para establecer una conexión de suscripción. Si la autenticación falla, puedes lanzar un error para denegar la conexión.
// ... en la configuración de SubscriptionServer o useServer
onConnect: (connectionParams, webSocket) => {
const authToken = connectionParams.authToken; // El cliente debe enviar esto
if (!authToken) {
throw new Error('Authentication token is required for subscriptions');
}
// Aquí, verifica el token (JWT, etc.) y devuelve el contexto del usuario
const user = verifyToken(authToken);
if (!user) {
throw new Error('Invalid authentication token');
}
return { currentUser: user };
},
Luego, en tus Subscription resolvers, puedes acceder a este currentUser a través del context:
Subscription: {
nuevoMensaje: {
subscribe: withFilter(
() => pubsub.asyncIterator([MESSAGE_ADDED]),
(payload, variables, context) => {
// Ejemplo de autorización: solo usuarios conectados pueden ver mensajes
if (!context.currentUser) {
return false;
}
// También puedes filtrar por roles o permisos específicos del usuario
return payload.nuevoMensaje.canalId === variables.canalId;
}
),
},
},
La función withFilter de graphql-subscriptions es muy útil para agregar lógica de filtrado server-side a tus suscripciones, permitiendo que solo los clientes relevantes reciban los eventos.
Optimización y Manejo de Errores
- Backpressure: En escenarios de alto volumen, un servidor puede producir eventos más rápido de lo que un cliente puede consumirlos. Implementar estrategias de backpressure es crucial para evitar el agotamiento de recursos. Esto a menudo se maneja a nivel de la capa de WebSocket o la implementación de Pub/Sub.
- Errores en Resolvers: Asegúrate de que tus resolvers de suscripción manejen los errores graciosamente. Cualquier error lanzado en un resolver de
subscribeoresolvese comunicará al cliente, pero no debe tumbar el servidor. - Cierre de Conexiones: El servidor debe ser robusto ante la desconexión inesperada de clientes y cerrar los iteradores de forma apropiada. Las librerías como
subscriptions-transport-wsygraphql-wsmanejan gran parte de esto automáticamente.
✅ Casos de Uso Comunes para GraphQL Subscriptions
Las Subscriptions son ideales para cualquier aplicación que necesite actualizaciones de datos en tiempo real.
¿Subscriptions vs. Polling?
Polling (o encuestas) implica que el cliente realiza repetidamente una Query GraphQL a intervalos regulares para buscar nuevas actualizaciones. Si bien puede funcionar para algunas situaciones, es ineficiente:- Overhead de HTTP: Cada polling implica una nueva conexión HTTP, cabeceras, etc.
- Latencia: Los datos no son verdaderamente en tiempo real, sino tan rápido como el intervalo de polling.
- Over-fetching/Under-fetching: Es difícil predecir el momento exacto de un cambio, lo que lleva a pedir datos innecesariamente o a perder actualizaciones.
Las Subscriptions, al usar WebSockets, mantienen una conexión persistente y solo envían datos cuando realmente hay un cambio, lo que las hace mucho más eficientes y reactivas para el tiempo real.
📝 Resumen y Próximos Pasos
Has aprendido cómo GraphQL Subscriptions proporcionan una solución elegante y poderosa para construir aplicaciones en tiempo real. Hemos cubierto:
- Los fundamentos del tipo
Subscriptionen el esquema GraphQL. - La arquitectura de los resolvers de suscripción y el patrón Pub/Sub.
- Cómo configurar Apollo Server para manejar conexiones WebSocket.
- Cómo consumir suscripciones desde el cliente Apollo.
- Consideraciones de escalabilidad usando Redis y mecanismos de autenticación.
Implementar Subscriptions puede transformar una aplicación estática en una experiencia dinámica e interactiva para el usuario. Te animo a experimentar con el código proporcionado y adaptarlo a tus propios proyectos.
Recursos Adicionales
- Documentación oficial de Apollo Server - Subscriptions
- Documentación de graphql-subscriptions
- Documentación de graphql-redis-subscriptions
- Protocolo graphql-ws
¡Ahora tienes las herramientas para llevar tus aplicaciones GraphQL al siguiente nivel de interactividad y tiempo real! ¡Feliz codificación! ✨
Tutoriales relacionados
- Optimización de GraphQL con Dataloader: Estrategias para Evitar el Problema N+1intermediate15 min
- Optimización de Consultas GraphQL: Estrategias para APIs Más Rápidas y Eficientesintermediate12 min
- Uniones y Fragmentos GraphQL: Dominando la Composición de Datos Avanzadaintermediate18 min
- Gestionando Sesiones y Autenticación en GraphQL con JWT y Cookies Segurasintermediate20 min
- Explorando GraphQL: Un Viaje Práctico para Construir APIs Flexibles y Eficientesintermediate25 min
Comentarios (0)
Aún no hay comentarios. ¡Sé el primero!