Skip to content
IRC-CodingIRC-Coding
GraphQL APIGraphQL SchemasResolversSubscriptionsApollo GraphQL

Fundamentos de Desarrollo GraphQL API

Aprende GraphQL API con Schemas, Resolvers, Subscriptions y Apollo. Queries, Mutations y Type System con ejemplos prácticos.

S

schutzgeist

35 min read
Fundamentos de Desarrollo GraphQL API

Fundamentos del Desarrollo de APIs GraphQL: Esquemas, Resolvers, Subscriptions y Apollo

Este artículo te ofrece una introducción exhaustiva a los fundamentos del desarrollo de APIs GraphQL, incluyendo esquemas, resolvers, subscriptions y Apollo con ejemplos prácticos.

En Pocas Palabras

GraphQL es un lenguaje de consulta para APIs y un entorno de ejecución del lado del servidor que interpreta estas consultas usando un sistema de tipos para definir estructuras de datos.

¿Qué es GraphQL exactamente? ¿Un framework, software o una idea?

GraphQL no es un framework ni software listo para usar que descargues e implementes inmediatamente. Es un lenguaje de consulta y una especificación que describe con precisión cómo los clientes solicitan datos a un servidor, cómo el servidor procesa esas solicitudes y qué respuestas devuelve.

Piénsalo así: GraphQL es como SQL para bases de datos, pero para APIs. Define la sintaxis de las consultas y las reglas para su ejecución. Tú mismo debes implementar GraphQL en un lenguaje de programación o usar bibliotecas y frameworks existentes.

Apollo, por ejemplo, es una plataforma y framework popular que implementa GraphQL. Te proporciona Apollo Server para el backend y Apollo Client para el frontend. Existen otras implementaciones como Relay, GraphQL Yoga o urql. GraphQL en sí permanece independiente del lenguaje de programación o del framework que utilices.

En resumen: GraphQL es una idea y un estándar abierto para APIs que permite a los clientes solicitar exactamente los datos que necesitan.

Descripción Técnica Compacta

GraphQL es un lenguaje de consulta para APIs que permite a los clientes solicitar los datos exactos que requieren y obtenerlo todo en una única petición.

Componentes clave:

Esquema GraphQL

  • Types: Definir estructuras de datos
  • Queries: Operaciones de recuperación de datos
  • Mutations: Operaciones de modificación de datos
  • Subscriptions: Actualizaciones en tiempo real
  • Interfaces: Tipos reutilizables
  • Unions: Agrupaciones de tipos

Resolvers

  • Field Resolvers: Resolver datos para campos individuales
  • Type Resolvers: Implementaciones de tipos
  • Context: Contexto compartido para resolvers
  • Data Loaders: Evitar el problema N+1
  • Middleware: Autenticación y validación

Apollo GraphQL

  • Apollo Server: Servidor GraphQL para Node.js
  • Apollo Client: Cliente GraphQL para web y móvil
  • Apollo Federation: GraphQL distribuido
  • Apollo Studio: Monitoreo de GraphQL
  • Apollo Gateway: Puerta de enlace GraphQL

Puntos Clave para Evaluación

  • GraphQL: Lenguaje de consulta para APIs con sistema de tipos
  • Schema: Definiciones de tipos para estructuras de datos
  • Query: Operación de recuperación de datos con campos específicos
  • Mutation: Operaciones de modificación de datos
  • Subscription: Transmisión de datos en tiempo real
  • Resolver: Funciones para resolver datos
  • Apollo: Plataforma GraphQL para servidor y cliente
  • Type System: Tipado estricto para datos
  • IHK-relevant: Desarrollo y arquitectura de APIs modernas

Componentes Principales

  1. Schema Definition: Sistema de tipos GraphQL y SDL El esquema es el corazón de cualquier API GraphQL. Describe con el Schema Definition Language (SDL) qué estructuras de datos existen y qué operaciones están permitidas. Define Object Types, Queries, Mutations, Subscriptions, Enums, Interfaces y Unions. El esquema actúa como un contrato entre cliente y servidor.

  2. Query Execution: Análisis y ejecución de consultas Cuando un cliente envía una consulta, primero se analiza y se valida contra el esquema. El motor de ejecución determina entonces qué resolvers se invocan y en qué orden para devolver los datos solicitados. Se recorre el árbol de consultas GraphQL.

  3. Resolver Functions: Resolución de datos y lógica de negocio Cada campo en un esquema GraphQL puede tener un resolver. Los resolvers son funciones que suministran los datos reales para un campo. Pueden cargar datos de bases de datos, APIs externas u otras fuentes y frecuentemente contienen la lógica de negocio de la aplicación.

  4. Subscriptions: Actualizaciones en tiempo real basadas en WebSocket Las subscriptions permiten que el servidor envíe eventos en tiempo real a los clientes. Típicamente se basan en WebSockets. Se utilizan cuando los clientes deben ser informados inmediatamente de cambios, por ejemplo, nuevos mensajes o actualizaciones de estado.

  5. Apollo Server: Implementación del servidor GraphQL Apollo Server es un framework popular de Node.js que proporciona una API GraphQL basada en un esquema y resolvers. Ofrece características como autenticación, manejo de errores, subscriptions, caché e integración de plugins.

  6. Apollo Client: Cliente GraphQL con caché Apollo Client es una biblioteca para aplicaciones JavaScript/TypeScript que permite ejecutar consultas GraphQL, cachear datos y actualizar automáticamente componentes de la UI cuando los datos cambian. El caché reduce peticiones de red innecesarias.

  7. Data Loading: Optimización de consultas de datos En consultas GraphQL complejas puede surgir el problema N+1, cuando se ejecuta una consulta de base de datos separada para cada objeto. Los data loaders agrupan estas solicitudes y minimizan el número de accesos a la base de datos.

  8. Error Handling: Manejo de errores y validación GraphQL devuelve una respuesta incluso cuando hay campos con errores. Los errores se devuelven en un array errors separado. Además, se pueden manejar validaciones personalizadas, errores de autenticación y de autorización en los resolvers.

¿Qué es Apollo y por qué deberías usarlo?

Apollo es una plataforma integral para GraphQL, desarrollada por la empresa Apollo GraphQL Inc. Consta de varios componentes que juntos cubren todo el flujo de trabajo de GraphQL: Apollo Server para el backend, Apollo Client para el frontend, y Apollo Studio junto con Apollo Federation para monitoreo y arquitecturas distribuidas.

¿Dónde obtienes Apollo?

  • Apollo Server y Apollo Client están disponibles como paquetes de código abierto en npm.
  • Apollo Studio ofrece una interfaz basada en la nube en https://www.apollographql.com/.
  • La documentación y otras herramientas las encuentras en el sitio oficial de Apollo.

¿Quién usa Apollo y para qué?

  • Desarrolladores de backend usan Apollo Server para construir APIs GraphQL rápida y segura de tipos.
  • Desarrolladores de frontend usan Apollo Client para cargar y gestionar datos de APIs GraphQL en React, Vue, Angular u otros frameworks.
  • DevOps y arquitectos usan Apollo Studio y Federation para supervisar APIs, documentarlas y distribuirlas entre múltiples microservicios.

¿Por qué deberías usar Apollo?

  • Apollo se construye sobre el estándar GraphQL oficial y es uno de los stacks más utilizados.
  • Apollo Server simplifica la creación de esquemas, resolvers, autenticación y subscriptions.
  • Apollo Client reduce significativamente el esfuerzo en frontend mediante caché inteligente y gestión de estado.
  • Apollo Studio ayuda a encontrar errores, medir el rendimiento y documentar el esquema.
  • Apollo Federation permite construir APIs grandes a partir de servicios pequeños e independientes.

En definitiva: Apollo es el ecosistema más conocido alrededor de GraphQL y es ideal cuando quieres construir rápidamente una API lista para producción, bien documentada y escalable.

Ejemplos Prácticos

En los siguientes ejemplos verás cómo se construye una GraphQL API en la práctica. Primero programarás un servidor GraphQL completo con Apollo, Node.js, Express y Mongoose. El ejemplo muestra cómo definir un esquema, escribir resolvers, incorporar autenticación y realizar Subscriptions a través de WebSockets. Después viene un ejemplo para un Apollo Client en React que obtiene datos del servidor, los cachea y se actualiza automáticamente.

1. GraphQL Server con Apollo y Node.js

// server.js
const { ApolloServer, gql, AuthenticationError, ForbiddenError } = require('apollo-server-express');
const { ApolloServerPluginDrainHttpServer } = require('apollo-server-core');
const { makeExecutableSchema } = require('@graphql-tools/schema');
const { default: mongoose } = require('mongoose');
const express = require('express');
const http = require('http');
const cors = require('cors');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
const { PubSub } = require('graphql-subscriptions');
const { GraphQLUpload } = require('graphql-upload');
const { GraphQLJSON } = require('graphql-type-json');
const DataLoader = require('dataloader');

// Database Models
const userSchema = new mongoose.Schema({
  username: { type: String, required: true, unique: true },
  email: { type: String, required: true, unique: true },
  password: { type: String, required: true },
  firstName: String,
  lastName: String,
  avatar: String,
  role: { type: String, enum: ['user', 'admin'], default: 'user' },
  isActive: { type: Boolean, default: true },
  createdAt: { type: Date, default: Date.now },
  updatedAt: { type: Date, default: Date.now }
});

const postSchema = new mongoose.Schema({
  title: { type: String, required: true },
  content: { type: String, required: true },
  author: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
  tags: [String],
  status: { type: String, enum: ['draft', 'published', 'archived'], default: 'draft' },
  featured: { type: Boolean, default: false },
  likes: [{ type: mongoose.Schema.Types.ObjectId, ref: 'User' }],
  comments: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Comment' }],
  createdAt: { type: Date, default: Date.now },
  updatedAt: { type: Date, default: Date.now }
});

const commentSchema = new mongoose.Schema({
  content: { type: String, required: true },
  author: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
  post: { type: mongoose.Schema.Types.ObjectId, ref: 'Post', required: true },
  parent: { type: mongoose.Schema.Types.ObjectId, ref: 'Comment' },
  likes: [{ type: mongoose.Schema.Types.ObjectId, ref: 'User' }],
  createdAt: { type: Date, default: Date.now },
  updatedAt: { type: Date, default: Date.now }
});

const User = mongoose.model('User', userSchema);
const Post = mongoose.model('Post', postSchema);
const Comment = mongoose.model('Comment', commentSchema);

// GraphQL Schema Definition
const typeDefs = gql`
  scalar Upload
  scalar JSON
  scalar DateTime

  directive @auth(requires: String = "USER") on FIELD_DEFINITION
  directive @admin on FIELD_DEFINITION
  directive @rateLimit(limit: Int, duration: Int) on FIELD_DEFINITION

  type User {
    id: ID!
    username: String!
    email: String!
    firstName: String
    lastName: String
    avatar: String
    role: UserRole!
    isActive: Boolean!
    createdAt: DateTime!
    updatedAt: DateTime!
    posts(limit: Int, offset: Int): [Post!]!
    comments(limit: Int, offset: Int): [Comment!]!
    likedPosts: [Post!]!
    followers: [User!]!
    following: [User!]!
    postCount: Int!
    commentCount: Int!
    followerCount: Int!
    followingCount: Int!
  }

  type Post {
    id: ID!
    title: String!
    content: String!
    author: User!
    tags: [String!]!
    status: PostStatus!
    featured: Boolean!
    likes: [User!]!
    comments(limit: Int, offset: Int): [Comment!]!
    likeCount: Int!
    commentCount: Int!
    createdAt: DateTime!
    updatedAt: DateTime!
    isLiked: Boolean
  }

  type Comment {
    id: ID!
    content: String!
    author: User!
    post: Post!
    parent: Comment
    replies: [Comment!]!
    likes: [User!]!
    likeCount: Int!
    replyCount: Int!
    createdAt: DateTime!
    updatedAt: DateTime!
    isLiked: Boolean
  }

  enum UserRole {
    USER
    ADMIN
  }

  enum PostStatus {
    DRAFT
    PUBLISHED
    ARCHIVED
  }

  input UserInput {
    username: String!
    email: String!
    password: String!
    firstName: String
    lastName: String
  }

  input UserUpdateInput {
    username: String
    email: String
    firstName: String
    lastName: String
    avatar: String
  }

  input PostInput {
    title: String!
    content: String!
    tags: [String!]
    status: PostStatus
    featured: Boolean
  }

  input PostUpdateInput {
    title: String
    content: String
    tags: [String!]
    status: PostStatus
    featured: Boolean
  }

  input CommentInput {
    content: String!
    postId: ID!
    parentId: ID
  }

  input CommentUpdateInput {
    content: String
  }

  type AuthPayload {
    token: String!
    user: User!
  }

  type Query {
    # User queries
    me: User @auth
    user(id: ID!): User
    users(limit: Int = 10, offset: Int = 0, search: String): [User!]!
    
    # Post queries
    post(id: ID!): Post
    posts(
      limit: Int = 10
      offset: Int = 0
      status: PostStatus = PUBLISHED
      authorId: ID
      tags: [String]
      search: String
      featured: Boolean
    ): [Post!]!
    trendingPosts(limit: Int = 5): [Post!]!
    
    # Comment queries
    comment(id: ID!): Comment
    comments(postId: ID!, limit: Int = 10, offset: Int = 0): [Comment!]!
    
    # Search queries
    search(query: String!, type: String): SearchResult!
  }

  type Mutation {
    # Authentication mutations
    register(input: UserInput!): AuthPayload!
    login(username: String!, password: String!): AuthPayload!
    refreshToken: String! @auth
    
    # User mutations
    updateProfile(input: UserUpdateInput!): User! @auth
    changePassword(currentPassword: String!, newPassword: String!): Boolean! @auth
    followUser(userId: ID!): Boolean! @auth
    unfollowUser(userId: ID!): Boolean! @auth
    
    # Post mutations
    createPost(input: PostInput!): Post! @auth @rateLimit(limit: 5, duration: 60)
    updatePost(id: ID!, input: PostUpdateInput!): Post! @auth
    deletePost(id: ID!): Boolean! @auth
    likePost(postId: ID!): Post! @auth
    unlikePost(postId: ID!): Post! @auth
    
    # Comment mutations
    createComment(input: CommentInput!): Comment! @auth
    updateComment(id: ID!, input: CommentUpdateInput!): Comment! @auth
    deleteComment(id: ID!): Boolean! @auth
    likeComment(commentId: ID!): Comment! @auth
    unlikeComment(commentId: ID!): Comment! @auth
    
    # File upload mutations
    uploadAvatar(file: Upload!): String! @auth
  }

  type Subscription {
    # Post subscriptions
    postCreated: Post!
    postUpdated(postId: ID): Post!
    postDeleted(postId: ID): ID!
    
    # Comment subscriptions
    commentCreated(postId: ID): Comment!
    commentUpdated(commentId: ID): Comment!
    commentDeleted(commentId: ID): ID!
    
    # User subscriptions
    userOnline(userId: ID): User!
    userOffline(userId: ID): User!
  }

  union SearchResult = User | Post | Comment
`;

// Resolvers
const resolvers = {
  // Custom scalar resolvers
  Upload: GraphQLUpload,
  JSON: GraphQLJSON,
  DateTime: {
    serialize: (value) => new Date(value).toISOString(),
    parseValue: (value) => new Date(value),
    parseLiteral: (ast) => new Date(ast.value)
  },

  // Query resolvers
  Query: {
    me: async (parent, args, context) => {
      if (!context.user) {
        throw new AuthenticationError('You must be logged in');
      }
      return context.user;
    },

    user: async (parent, { id }, context) => {
      try {
        const user = await User.findById(id)
          .populate('followers following')
          .lean();
        
        if (!user || !user.isActive) {
          throw new Error('User not found');
        }
        
        return user;
      } catch (error) {
        throw new Error(`Failed to fetch user: ${error.message}`);
      }
    },

    users: async (parent, { limit = 10, offset = 0, search }, context) => {
      try {
        let query = { isActive: true };
        
        if (search) {
          query.$or = [
            { username: { $regex: search, $options: 'i' } },
            { email: { $regex: search, $options: 'i' } },
            { firstName: { $regex: search, $options: 'i' } },
            { lastName: { $regex: search, $options: 'i' } }
          ];
        }
        
        const users = await User.find(query)
          .populate('followers following')
          .sort({ createdAt: -1 })
          .limit(limit)
          .skip(offset)
          .lean();
        
        return users;
      } catch (error) {
        throw new Error(`Failed to fetch users: ${error.message}`);
      }
    },

    post: async (parent, { id }, context) => {
      try {
        const post = await Post.findById(id)
          .populate('author')
          .populate('comments')
          .lean();
        
        if (!post) {
          throw new Error('Post not found');
        }
        
        // Add isLiked field if user is authenticated
        if (context.user) {
          post.isLiked = post.likes.some(like => like.toString() === context.user.id);
        }
        
        return post;
      } catch (error) {
        throw new Error(`Failed to fetch post: ${error.message}`);
      }
    },

    posts: async (parent, args, context) => {
      try {
        const {
          limit = 10,
          offset = 0,
          status = 'PUBLISHED',
          authorId,
          tags,
          search,
          featured
        } = args;
        
        let query = { status };
        
        if (authorId) {
          query.author = authorId;
        }
        
        if (tags && tags.length > 0) {
          query.tags = { $in: tags };
        }
        
        if (search) {
          query.$or = [
            { title: { $regex: search, $options: 'i' } },
            { content: { $regex: search, $options: 'i' } }
          ];
        }
        
        if (featured !== undefined) {
          query.featured = featured;
        }
        
        const posts = await Post.find(query)
          .populate('author')
          .sort({ createdAt: -1 })
          .limit(limit)
          .skip(offset)
          .lean();
        
        // Add isLiked field if user is authenticated
        if (context.user) {
          posts.forEach(post => {
            post.isLiked = post.likes.some(like => like.toString() === context.user.id);
          });
        }
        
        return posts;
      } catch (error) {
        throw new Error(`Failed to fetch posts: ${error.message}`);
      }
    },

    trendingPosts: async (parent, { limit = 5 }, context) => {
      try {
        const posts = await Post.find({ status: 'PUBLISHED' })
          .populate('author')
          .sort({ likes: -1, createdAt: -1 })
          .limit(limit)
          .lean();
        
        // Add isLiked field if user is authenticated
        if (context.user) {
          posts.forEach(post => {
            post.isLiked = post.likes.some(like => like.toString() === context.user.id);
          });
        }
        
        return posts;
      } catch (error) {
        throw new Error(`Failed to fetch trending posts: ${error.message}`);
      }
    },

    comment: async (parent, { id }, context) => {
      try {
        const comment = await Comment.findById(id)
          .populate('author')
          .populate('post')
          .populate('parent')
          .lean();
        
        if (!comment) {
          throw new Error('Comment not found');
        }
        
        // Add isLiked field if user is authenticated
        if (context.user) {
          comment.isLiked = comment.likes.some(like => like.toString() === context.user.id);
        }
        
        return comment;
      } catch (error) {
        throw new Error(`Failed to fetch comment: ${error.message}`);
      }
    },

    comments: async (parent, { postId, limit = 10, offset = 0 }, context) => {
      try {
        const comments = await Comment.find({ post: postId, parent: null })
          .populate('author')
          .populate('replies')
          .sort({ createdAt: -1 })
          .limit(limit)
          .skip(offset)
          .lean();
        
        // Add isLiked field if user is authenticated
        if (context.user) {
          comments.forEach(comment => {
            comment.isLiked = comment.likes.some(like => like.toString() === context.user.id);
          });
        }
        
        return comments;
      } catch (error) {
        throw new Error(`Failed to fetch comments: ${error.message}`);
      }
    },

    search: async (parent, { query, type }, context) => {
      try {
        const searchRegex = { $regex: query, $options: 'i' };
        
        let results = [];
        
        if (!type || type === 'USER') {
          const users = await User.find({
            $or: [
              { username: searchRegex },
              { email: searchRegex },
              { firstName: searchRegex },
              { lastName: searchRegex }
            ],
            isActive: true
          }).limit(5).lean();
          
          results.push(...users);
        }
        
        if (!type || type === 'POST') {
          const posts = await Post.find({
            $or: [
              { title: searchRegex },
              { content: searchRegex },
              { tags: searchRegex }
            ],
            status: 'PUBLISHED'
          }).populate('author').limit(5).lean();
          
          results.push(...posts);
        }
        
        if (!type || type === 'COMMENT') {
          const comments = await Comment.find({
            content: searchRegex
          }).populate('author').populate('post').limit(5).lean();
          
          results.push(...comments);
        }
        
        return results;
      } catch (error) {
        throw new Error(`Search failed: ${error.message}`);
      }
    }
  },

  // Mutation resolvers
  Mutation: {
    register: async (parent, { input }, context) => {
      try {
        const { username, email, password, firstName, lastName } = input;
        
        // Check if user already exists
        const existingUser = await User.findOne({
          $or: [{ username }, { email }]
        });
        
        if (existingUser) {
          throw new Error('User already exists');
        }
        
        // Hash password
        const hashedPassword = await bcrypt.hash(password, 10);
        
        // Create user
        const user = new User({
          username,
          email,
          password: hashedPassword,
          firstName,
          lastName
        });
        
        await user.save();
        
        // Generate JWT token
        const token = jwt.sign(
          { userId: user._id, username: user.username },
          process.env.JWT_SECRET,
          { expiresIn: '7d' }
        );
        
        // Publish user created event
        pubsub.publish('USER_CREATED', {
          userCreated: user
        });
        
        return { token, user };
      } catch (error) {
        throw new Error(`Registration failed: ${error.message}`);
      }
    },

    login: async (parent, { username, password }, context) => {
      try {
        // Find user
        const user = await User.findOne({ username, isActive: true });
        if (!user) {
          throw new Error('Invalid credentials');
        }
        
        // Verify password
        const isValidPassword = await bcrypt.compare(password, user.password);
        if (!isValidPassword) {
          throw new Error('Invalid credentials');
        }
        
        // Generate JWT token
        const token = jwt.sign(
          { userId: user._id, username: user.username },
          process.env.JWT_SECRET,
          { expiresIn: '7d' }
        );
        
        return { token, user };
      } catch (error) {
        throw new Error(`Login failed: ${error.message}`);
      }
    },

    refreshToken: async (parent, args, context) => {
      try {
        if (!context.user) {
          throw new AuthenticationError('Invalid token');
        }
        
        // Generate new token
        const token = jwt.sign(
          { userId: context.user._id, username: context.user.username },
          process.env.JWT_SECRET,
          { expiresIn: '7d' }
        );
        
        return token;
      } catch (error) {
        throw new Error(`Token refresh failed: ${error.message}`);
      }
    },

    updateProfile: async (parent, { input }, context) => {
      try {
        const { username, email, firstName, lastName, avatar } = input;
        
        // Check if username or email is already taken
        if (username || email) {
          const existingUser = await User.findOne({
            _id: { $ne: context.user._id },
            $or: [
              ...(username ? [{ username }] : []),
              ...(email ? [{ email }] : [])
            ]
          });
          
          if (existingUser) {
            throw new Error('Username or email already exists');
          }
        }
        
        // Update user
        const updatedUser = await User.findByIdAndUpdate(
          context.user._id,
          {
            ...(username && { username }),
            ...(email && { email }),
            ...(firstName && { firstName }),
            ...(lastName && { lastName }),
            ...(avatar && { avatar }),
            updatedAt: new Date()
          },
          { new: true }
        ).lean();
        
        return updatedUser;
      } catch (error) {
        throw new Error(`Profile update failed: ${error.message}`);
      }
    },

    changePassword: async (parent, { currentPassword, newPassword }, context) => {
      try {
        // Get user with password
        const user = await User.findById(context.user._id);
        
        // Verify current password
        const isValidPassword = await bcrypt.compare(currentPassword, user.password);
        if (!isValidPassword) {
          throw new Error('Current password is incorrect');
        }
        
        // Hash new password
        const hashedNewPassword = await bcrypt.hash(newPassword, 10);
        
        // Update password
        await User.findByIdAndUpdate(context.user._id, {
          password: hashedNewPassword,
          updatedAt: new Date()
        });
        
        return true;
      } catch (error) {
        throw new Error(`Password change failed: ${error.message}`);
      }
    },

    createPost: async (parent, { input }, context) => {
      try {
        const post = new Post({
          ...input,
          author: context.user._id
        });
        
        await post.save();
        await post.populate('author');
        
        // Publish post created event
        pubsub.publish('POST_CREATED', {
          postCreated: post
        });
        
        return post;
      } catch (error) {
        throw new Error(`Post creation failed: ${error.message}`);
      }
    },

    updatePost: async (parent, { id, input }, context) => {
      try {
        const post = await Post.findById(id);
        
        if (!post) {
          throw new Error('Post not found');
        }
        
        // Check if user is the author or admin
        if (post.author.toString() !== context.user._id && context.user.role !== 'ADMIN') {
          throw new ForbiddenError('Not authorized to update this post');
        }
        
        const updatedPost = await Post.findByIdAndUpdate(
          id,
          {
            ...input,
            updatedAt: new Date()
          },
          { new: true }
        ).populate('author');
        
        // Publish post updated event
        pubsub.publish('POST_UPDATED', {
          postUpdated: updatedPost,
          postId: id
        });
        
        return updatedPost;
      } catch (error) {
        throw new Error(`Post update failed: ${error.message}`);
      }
    },

    deletePost: async (parent, { id }, context) => {
      try {
        const post = await Post.findById(id);
        
        if (!post) {
          throw new Error('Post not found');
        }
        
        // Check if user is the author or admin
        if (post.author.toString() !== context.user._id && context.user.role !== 'ADMIN') {
          throw new ForbiddenError('Not authorized to delete this post');
        }
        
        await Post.findByIdAndDelete(id);
        
        // Publish post deleted event
        pubsub.publish('POST_DELETED', {
          postDeleted: id,
          postId: id
        });
        
        return true;
      } catch (error) {
        throw new Error(`Post deletion failed: ${error.message}`);
      }
    },

    likePost: async (parent, { postId }, context) => {
      try {
        const post = await Post.findById(postId);
        
        if (!post) {
          throw new Error('Post not found');
        }
        
        // Check if already liked
        if (post.likes.includes(context.user._id)) {
          throw new Error('Post already liked');
        }
        
        // Add like
        post.likes.push(context.user._id);
        await post.save();
        await post.populate('author');
        
        return post;
      } catch (error) {
        throw new Error(`Post like failed: ${error.message}`);
      }
    },

    unlikePost: async (parent, { postId }, context) => {
      try {
        const post = await Post.findById(postId);
        
        if (!post) {
          throw new Error('Post not found');
        }
        
        // Check if not liked
        if (!post.likes.includes(context.user._id)) {
          throw new Error('Post not liked');
        }
        
        // Remove like
        post.likes = post.likes.filter(like => like.toString() !== context.user._id);
        await post.save();
        await post.populate('author');
        
        return post;
      } catch (error) {
        throw new Error(`Post unlike failed: ${error.message}`);
      }
    },

    createComment: async (parent, { input }, context) => {
      try {
        const { content, postId, parentId } = input;
        
        // Verify post exists
        const post = await Post.findById(postId);
        if (!post) {
          throw new Error('Post not found');
        }
        
        // Verify parent comment exists if provided
        if (parentId) {
          const parentComment = await Comment.findById(parentId);
          if (!parentComment) {
            throw new Error('Parent comment not found');
          }
        }
        
        const comment = new Comment({
          content,
          author: context.user._id,
          post: postId,
          parent: parentId
        });
        
        await comment.save();
        await comment.populate('author post parent');
        
        // Publish comment created event
        pubsub.publish('COMMENT_CREATED', {
          commentCreated: comment,
          postId
        });
        
        return comment;
      } catch (error) {
        throw new Error(`Comment creation failed: ${error.message}`);
      }
    },

    updateComment: async (parent, { id, input }, context) => {
      try {
        const comment = await Comment.findById(id);
        
        if (!comment) {
          throw new Error('Comment not found');
        }
        
        // Check if user is the author or admin
        if (comment.author.toString() !== context.user._id && context.user.role !== 'ADMIN') {
          throw new ForbiddenError('Not authorized to update this comment');
        }
        
        const updatedComment = await Comment.findByIdAndUpdate(
          id,
          {
            ...input,
            updatedAt: new Date()
          },
          { new: true }
        ).populate('author post parent');
        
        // Publish comment updated event
        pubsub.publish('COMMENT_UPDATED', {
          commentUpdated: updatedComment,
          commentId: id
        });
        
        return updatedComment;
      } catch (error) {
        throw new Error(`Comment update failed: ${error.message}`);
      }
    },

    deleteComment: async (parent, { id }, context) => {
      try {
        const comment = await Comment.findById(id);
        
        if (!comment) {
          throw new Error('Comment not found');
        }
        
        // Check if user is the author or admin
        if (comment.author.toString() !== context.user._id && context.user.role !== 'ADMIN') {
          throw new ForbiddenError('Not authorized to delete this comment');
        }
        
        await Comment.findByIdAndDelete(id);
        
        // Publish comment deleted event
        pubsub.publish('COMMENT_DELETED', {
          commentDeleted: id,
          commentId: id
        });
        
        return true;
      } catch (error) {
        throw new Error(`Comment deletion failed: ${error.message}`);
      }
    }
  },

  // Subscription resolvers
  Subscription: {
    postCreated: {
      subscribe: () => pubsub.asyncIterator(['POST_CREATED'])
    },

    postUpdated: {
      subscribe: (parent, { postId }) => {
        if (postId) {
          return pubsub.asyncIterator([`POST_UPDATED_${postId}`]);
        }
        return pubsub.asyncIterator(['POST_UPDATED']);
      }
    },

    postDeleted: {
      subscribe: (parent, { postId }) => {
        if (postId) {
          return pubsub.asyncIterator([`POST_DELETED_${postId}`]);
        }
        return pubsub.asyncIterator(['POST_DELETED']);
      }
    },

    commentCreated: {
      subscribe: (parent, { postId }) => {
        if (postId) {
          return pubsub.asyncIterator([`COMMENT_CREATED_${postId}`]);
        }
        return pubsub.asyncIterator(['COMMENT_CREATED']);
      }
    },

    commentUpdated: {
      subscribe: (parent, { commentId }) => {
        if (commentId) {
          return pubsub.asyncIterator([`COMMENT_UPDATED_${commentId}`]);
        }
        return pubsub.asyncIterator(['COMMENT_UPDATED']);
      }
    },

    commentDeleted: {
      subscribe: (parent, { commentId }) => {
        if (commentId) {
          return pubsub.asyncIterator([`COMMENT_DELETED_${commentId}`]);
        }
        return pubsub.asyncIterator(['COMMENT_DELETED']);
      }
    }
  },

  // Field resolvers
  User: {
    posts: async (parent, { limit = 10, offset = 0 }, context) => {
      try {
        return await Post.find({ author: parent._id })
          .sort({ createdAt: -1 })
          .limit(limit)
          .skip(offset)
          .lean();
      } catch (error) {
        throw new Error(`Failed to fetch user posts: ${error.message}`);
      }
    },

    comments: async (parent, { limit = 10, offset = 0 }, context) => {
      try {
        return await Comment.find({ author: parent._id })
          .sort({ createdAt: -1 })
          .limit(limit)
          .skip(offset)
          .lean();
      } catch (error) {
        throw new Error(`Failed to fetch user comments: ${error.message}`);
      }
    },

    likedPosts: async (parent, args, context) => {
      try {
        return await Post.find({ likes: parent._id })
          .populate('author')
          .sort({ createdAt: -1 })
          .lean();
      } catch (error) {
        throw new Error(`Failed to fetch liked posts: ${error.message}`);
      }
    },

    followers: async (parent, args, context) => {
      try {
        return await User.find({ following: parent._id })
          .sort({ createdAt: -1 })
          .lean();
      } catch (error) {
        throw new Error(`Failed to fetch followers: ${error.message}`);
      }
    },

    following: async (parent, args, context) => {
      try {
        return await User.find({ followers: parent._id })
          .sort({ createdAt: -1 })
          .lean();
      
### 2. Cliente GraphQL con Apollo Client y React
```jsx
// ApolloClient.js
import { ApolloClient, InMemoryCache, createHttpLink, from } from '@apollo/client';
import { setContext } from '@apollo/client/link/context';
import { onError } from '@apollo/client/link/error';
import { split } from '@apollo/client';
import { getMainDefinition } from '@apollo/client/utilities';
import { WebSocketLink } from '@apollo/client/link/ws';

// HTTP link
const httpLink = createHttpLink({
  uri: process.env.REACT_APP_GRAPHQL_URI || 'http://localhost:4000/graphql'
});

// WebSocket link for subscriptions
const wsLink = new WebSocketLink({
  uri: process.env.REACT_APP_GRAPHQL_WS_URI || 'ws://localhost:4000/graphql',
  options: {
    reconnect: true,
    connectionParams: () => {
      const token = localStorage.getItem('token');
      return {
        authorization: token ? `Bearer ${token}` : ''
      };
    }
  }
});

// Auth link
const authLink = setContext((_, { headers }) => {
  const token = localStorage.getItem('token');
  
  return {
    headers: {
      ...headers,
      authorization: token ? `Bearer ${token}` : ''
    }
  };
});

// Error handling link
const errorLink = onError(({ graphQLErrors, networkError, operation, forward }) => {
  if (graphQLErrors) {
    graphQLErrors.forEach(({ message, locations, path, extensions }) => {
      console.error(
        `[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`,
        extensions
      );
      
      // Handle authentication errors
      if (extensions?.code === 'UNAUTHENTICATED') {
        localStorage.removeItem('token');
        window.location.href = '/login';
      }
      
      // Handle rate limiting
      if (extensions?.code === 'RATE_LIMIT_EXCEEDED') {
        console.warn('Rate limit exceeded. Please try again later.');
      }
    });
  }
  
  if (networkError) {
    console.error(`[Network error]: ${networkError}`);
    
    // Handle network errors
    if (networkError.statusCode === 401) {
      localStorage.removeItem('token');
      window.location.href = '/login';
    }
  }
});

// Split link for subscriptions
const splitLink = split(
  ({ query }) => {
    const definition = getMainDefinition(query);
    return (
      definition.kind === 'OperationDefinition' &&
      definition.operation === 'subscription'
    );
  },
  wsLink,
  from([errorLink, authLink, httpLink])
);

// Apollo Client instance
export const client = new ApolloClient({
  link: splitLink,
  cache: new InMemoryCache({
    typePolicies: {
      Query: {
        fields: {
          posts: {
            merge(existing = [], incoming) {
              return [...incoming];
            }
          },
          users: {
            merge(existing = [], incoming) {
              return [...incoming];
            }
          }
        }
      },
      User: {
        fields: {
          posts: {
            merge(existing = [], incoming) {
              return [...incoming];
            }
          },
          followers: {
            merge(existing = [], incoming) {
              return [...incoming];
            }
          },
          following: {
            merge(existing = [], incoming) {
              return [...incoming];
            }
          }
        }
      },
      Post: {
        fields: {
          comments: {
            merge(existing = [], incoming) {
              return [...incoming];
            }
          },
          likes: {
            merge(existing = [], incoming) {
              return [...incoming];
            }
          }
        }
      },
      Comment: {
        fields: {
          replies: {
            merge(existing = [], incoming) {
              return [...incoming];
            }
          },
          likes: {
            merge(existing = [], incoming) {
              return [...incoming];
            }
          }
        }
      }
    }
  }),
  defaultOptions: {
    watchQuery: {
      errorPolicy: 'all',
      notifyOnNetworkStatusChange: true
    },
    query: {
      errorPolicy: 'all'
    }
  }
});

export default client;

// GraphQL Queries
import { gql } from '@apollo/client';

export const GET_ME = gql`
  query GetMe {
    me {
      id
      username
      email
      firstName
      lastName
      avatar
      role
      isActive
      createdAt
      updatedAt
      postCount
      commentCount
      followerCount
      followingCount
    }
  }
`;

export const GET_USER = gql`
  query GetUser($id: ID!) {
    user(id: $id) {
      id
      username
      email
      firstName
      lastName
      avatar
      role
      isActive
      createdAt
      updatedAt
      postCount
      commentCount
      followerCount
      followingCount
      followers {
        id
        username
        firstName
        lastName
        avatar
      }
      following {
        id
        username
        firstName
        lastName
        avatar
      }
    }
  }
`;

export const GET_USERS = gql`
  GetUsers($limit: Int, $offset: Int, $search: String) {
    users(limit: $limit, offset: $offset, search: $search) {
      id
      username
      email
      firstName
      lastName
      avatar
      role
      isActive
      createdAt
      updatedAt
      postCount
      commentCount
      followerCount
      followingCount
    }
  }
`;

export const GET_POST = gql`
  query GetPost($id: ID!) {
    post(id: $id) {
      id
      title
      content
      status
      featured
      createdAt
      updatedAt
      likeCount
      commentCount
      isLiked
      author {
        id
        username
        firstName
        lastName
        avatar
      }
      tags
    }
  }
`;

export const GET_POSTS = gql`
  query GetPosts(
    $limit: Int
    $offset: Int
    $status: PostStatus
    $authorId: ID
    $tags: [String]
    $search: String
    $featured: Boolean
  ) {
    posts(
      limit: $limit
      offset: $offset
      status: $status
      authorId: $authorId
      tags: $tags
      search: $search
      featured: $featured
    ) {
      id
      title
      content
      status
      featured
      createdAt
      updatedAt
      likeCount
      commentCount
      isLiked
      author {
        id
        username
        firstName
        lastName
        avatar
      }
      tags
    }
  }
`;

export const GET_TRENDING_POSTS = gql`
  query GetTrendingPosts($limit: Int) {
    trendingPosts(limit: $limit) {
      id
      title
      content
      status
      featured
      createdAt
      updatedAt
      likeCount
      commentCount
      isLiked
      author {
        id
        username
        firstName
        lastName
        avatar
      }
      tags
    }
  }
`;

export const GET_COMMENTS = gql`
  query GetComments($postId: ID!, $limit: Int, $offset: Int) {
    comments(postId: $postId, limit: $limit, offset: $offset) {
      id
      content
      createdAt
      updatedAt
      likeCount
      replyCount
      isLiked
      author {
        id
        username
        firstName
        lastName
        avatar
      }
      replies {
        id
        content
        createdAt
        updatedAt
        likeCount
        isLiked
        author {
          id
          username
          firstName
          lastName
          avatar
        }
      }
    }
  }
`;

export const SEARCH = gql`
  query Search($query: String!, $type: String) {
    search(query: $query, type: $type) {
      ... on User {
        id
        username
        email
        firstName
        lastName
        avatar
        role
        postCount
        commentCount
        followerCount
        followingCount
      }
      ... on Post {
        id
        title
        content
        status
        featured
        createdAt
        likeCount
        commentCount
        author {
          id
          username
          firstName
          lastName
          avatar
        }
        tags
      }
      ... on Comment {
        id
        content
        createdAt
        likeCount
        author {
          id
          username
          firstName
          lastName
          avatar
        }
        post {
          id
          title
        }
      }
    }
  }
`;

// GraphQL Mutations
export const REGISTER = gql`
  mutation Register($input: UserInput!) {
    register(input: $input) {
      token
      user {
        id
        username
        email
        firstName
        lastName
        avatar
        role
        isActive
        createdAt
        updatedAt
      }
    }
  }
`;

export const LOGIN = gql`
  mutation Login($username: String!, $password: String!) {
    login(username: $username, password: $password) {
      token
      user {
        id
        username
        email
        firstName
        lastName
        avatar
        role
        isActive
        createdAt
        updatedAt
      }
    }
  }
`;

export const UPDATE_PROFILE = gql`
  mutation UpdateProfile($input: UserUpdateInput!) {
    updateProfile(input: $input) {
      id
      username
      email
      firstName
      lastName
      avatar
      role
      isActive
      createdAt
      updatedAt
    }
  }
`;

export const CHANGE_PASSWORD = gql`
  mutation ChangePassword($currentPassword: String!, $newPassword: String!) {
    changePassword(currentPassword: $currentPassword, newPassword: $newPassword)
  }
`;

export const CREATE_POST = gql`
  mutation CreatePost($input: PostInput!) {
    createPost(input: $input) {
      id
      title
      content
      status
      featured
      createdAt
      updatedAt
      likeCount
      commentCount
      author {
        id
        username
        firstName
        lastName
        avatar
      }
      tags
    }
  }
`;

export const UPDATE_POST = gql`
  mutation UpdatePost($id: ID!, $input: PostUpdateInput!) {
    updatePost(id: $id, input: $input) {
      id
      title
      content
      status
      featured
      createdAt
      updatedAt
      likeCount
      commentCount
      author {
        id
        username
        firstName
        lastName
        avatar
      }
      tags
    }
  }
`;

export const DELETE_POST = gql`
  mutation DeletePost($id: ID!) {
    deletePost(id: $id)
  }
`;

export const LIKE_POST = gql`
  mutation LikePost($postId: ID!) {
    likePost(postId: $postId) {
      id
      title
      content
      status
      featured
      createdAt
      updatedAt
      likeCount
      commentCount
      isLiked
      author {
        id
        username
        firstName
        lastName
        avatar
      }
      tags
    }
  }
`;

export const UNLIKE_POST = gql`
  mutation UnlikePost($postId: ID!) {
    unlikePost(postId: $postId) {
      id
      title
      content
      status
      featured
      createdAt
      updatedAt
      likeCount
      commentCount
      isLiked
      author {
        id
        username
        firstName
        lastName
        avatar
      }
      tags
    }
  }
`;

export const CREATE_COMMENT = gql`
  mutation CreateComment($input: CommentInput!) {
    createComment(input: $input) {
      id
      content
      createdAt
      updatedAt
      likeCount
      replyCount
      author {
        id
        username
        firstName
        lastName
        avatar
      }
      post {
        id
        title
      }
      parent {
        id
        content
        author {
          id
          username
          firstName
          lastName
          avatar
        }
      }
    }
  }
`;

export const UPDATE_COMMENT = gql`
  mutation UpdateComment($id: ID!, $input: CommentUpdateInput!) {
    updateComment(id: $id, input: $input) {
      id
      content
      createdAt
      updatedAt
      likeCount
      replyCount
      author {
        id
        username
        firstName
        lastName
        avatar
      }
      post {
        id
        title
      }
    }
  }
`;

export const DELETE_COMMENT = gql`
  mutation DeleteComment($id: ID!) {
    deleteComment(id: $id)
  }
`;

export const LIKE_COMMENT = gql`
  mutation LikeComment($commentId: ID!) {
    likeComment(commentId: $commentId) {
      id
      content
      createdAt
      updatedAt
      likeCount
      replyCount
      isLiked
      author {
        id
        username
        firstName
        lastName
        avatar
      }
      post {
        id
        title
      }
    }
  }
`;

export const UNLIKE_COMMENT = gql`
  mutation UnlikeComment($commentId: ID!) {
    unlikeComment(commentId: $commentId) {
      id
      content
      createdAt
      updatedAt
      likeCount
      replyCount
      isLiked
      author {
        id
        username
        firstName
        lastName
        avatar
      }
      post {
        id
        title
      }
    }
  }
`;

// GraphQL Subscriptions
export const POST_CREATED = gql`
  subscription PostCreated {
    postCreated {
      id
      title
      content
      status
      featured
      createdAt
      updatedAt
      likeCount
      commentCount
      author {
        id
        username
        firstName
        lastName
        avatar
      }
      tags
    }
  }
`;

export const POST_UPDATED = gql`
  subscription PostUpdated($postId: ID) {
    postUpdated(postId: $postId) {
      id
      title
      content
      status
      featured
      createdAt
      updatedAt
      likeCount
      commentCount
      author {
        id
        username
        firstName
        lastName
        avatar
      }
      tags
    }
  }
`;

export const POST_DELETED = gql`
  subscription PostDeleted($postId: ID) {
    postDeleted(postId: $postId)
  }
`;

export const COMMENT_CREATED = gql`
  subscription CommentCreated($postId: ID) {
    commentCreated(postId: $postId) {
      id
      content
      createdAt
      updatedAt
      likeCount
      replyCount
      author {
        id
        username
        firstName
        lastName
        avatar
      }
      post {
        id
        title
      }
      parent {
        id
        content
        author {
          id
          username
          firstName
          lastName
          avatar
        }
      }
    }
  }
`;

export const COMMENT_UPDATED = gql`
  subscription CommentUpdated($commentId: ID) {
    commentUpdated(commentId: $commentId) {
      id
      content
      createdAt
      updatedAt
      likeCount
      replyCount
      author {
        id
        username
        firstName
        lastName
        avatar
      }
      post {
        id
        title
      }
    }
  }
`;

export const COMMENT_DELETED = gql`
  subscription CommentDeleted($commentId: ID) {
    commentDeleted(commentId: $commentId)
  }
`;

// React Components
import React, { useState, useEffect } from 'react';
import { useQuery, useMutation, useSubscription } from '@apollo/client';
import { GET_POSTS, CREATE_POST, LIKE_POST, UNLIKE_POST, POST_CREATED } from './graphql';

// PostList Component
const PostList = ({ limit = 10, status = 'PUBLISHED' }) => {
  const [posts, setPosts] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  const [offset, setOffset] = useState(0);
  const [hasMore, setHasMore] = useState(true);

  const { data, loading: queryLoading, error: queryError, fetchMore } = useQuery(GET_POSTS, {
    variables: { limit, offset: 0, status },
    notifyOnNetworkStatusChange: true
  });

  const [createPost] = useMutation(CREATE_POST, {
    onCompleted: (data) => {
      setPosts(prev => [data.createPost, ...prev]);
    },
    onError: (error) => {
      console.error('Create post error:', error);
    }
  });

  const [likePost] = useMutation(LIKE_POST);
  const [unlikePost] = useMutation(UNLIKE_POST);

  // Subscription for new posts
  const { data: subscriptionData } = useSubscription(POST_CREATED);

  useEffect(() => {
    if (data) {
      setPosts(data.posts);
      setLoading(false);
    }
  }, [data]);

  useEffect(() => {
    if (queryError) {
      setError(queryError);
      setLoading(false);
    }
  }, [queryError]);

  useEffect(() => {
    if (subscriptionData) {
      setPosts(prev => [subscriptionData.postCreated, ...prev]);
    }
  }, [subscriptionData]);

  const handleLikePost = async (postId, isLiked) => {
    try {
      if (isLiked) {
        await unlikePost({ variables: { postId } });
      } else {
        await likePost({ variables: { postId } });
      }
      
      // Update local state
      setPosts(prev => prev.map(post => 
        post.id === postId 
          ? { ...post, isLiked: !isLiked, likeCount: isLiked ? post.likeCount - 1 : post.likeCount + 1 }
          : post
      ));
    } catch (error) {
      console.error('Like post error:', error);
    }
  };

  const loadMore = () => {
    if (!hasMore || queryLoading) return;

    fetchMore({
      variables: { offset: posts.length },
      updateQuery: (prev, { fetchMoreResult }) => {
        if (!fetchMoreResult) return prev;
        
        const newPosts = fetchMoreResult.posts;
        setHasMore(newPosts.length >= limit);
        
        return {
          posts: [...prev.posts, ...newPosts]
        };
      }
    });
  };

  if (loading && posts.length === 0) {
    return <div>Loading posts...</div>;
  }

  if (error) {
    return <div>Error: {error.message}</div>;
  }

  return (
    <div className="post-list">
      {posts.map(post => (
        <PostItem 
          key={post.id} 
          post={post} 
          onLike={handleLikePost}
        />
      ))}
      
      {hasMore && (
        <button onClick={loadMore} disabled={queryLoading}>
          {queryLoading ? 'Loading...' : 'Load More'}
        </button>
      )}
    </div>
  );
};

// PostItem Component
const PostItem = ({ post, onLike }) => {
  const [expanded, setExpanded] = useState(false);

  const handleLike = () => {
    onLike(post.id, post.isLiked);
  };

  return (
    <div className="post-item">
      <div className="post-header">
        <img 
          src={post.author.avatar || '/default-avatar.png'} 
          alt={post.author.username}
          className="author-avatar"
        />
        <div className="author-info">
          <h4>{post.author.firstName} {post.author.lastName}</h4>
          <p>@{post.author.username}</p>
        </div>
        <div className="post-meta">
          <span className="post-date">
            {new Date(post.createdAt).toLocaleDateString()}
          </span>
          {post.featured && <span className="featured-badge">Featured</span>}
        </div>
      </div>
      
      <div className="post-content">
        <h3>{post.title}</h3>
        <p className={expanded ? 'expanded' : 'collapsed'}>
          {post.content}
        </p>
        {post.content.length > 200 && (
          <button 
            onClick={() => setExpanded(!expanded)}
            className="expand-button"
          >
            {expanded ? 'Show Less' : 'Show More'}
          </button>
        )}
      </div>
      
      <div className="post-tags">
        {post.tags.map(tag => (
          <span key={tag} className="tag">
            #{tag}
          </span>
        ))}
      </div>
      
      <div className="post-actions">
        <button 
          onClick={handleLike}
          className={`like-button ${post.isLiked ? 'liked' : ''}`}
        >
          {post.isLiked ? '❤️' : '🤍'} {post.likeCount}
        </button>
        <button className="comment-button">
          💬 {post.commentCount}
        </button>
        <button className="share-button">
          🔗 Share
        </button>
      </div>
    </div>
  );
};

// CreatePost Component
const CreatePost = () => {
  const [title, setTitle] = useState('');
  const [content, setContent] = useState('');
  const [tags, setTags] = useState('');
  const [status, setStatus] = useState('PUBLISHED');
  const [loading, setLoading] = useState(false);

  const [createPost] = useMutation(CREATE_POST);

  const handleSubmit = async (e) => {
    e.preventDefault();
    setLoading(true);

    try {
      await createPost({
        variables: {
          input: {
            title,
            content,
            tags: tags.split(',').map(tag => tag.trim()).filter(Boolean),
            status
          }
        }
      });

      // Reset form
      setTitle('');
      setContent('');
      setTags('');
      setStatus('PUBLISHED');
    } catch (error) {
      console.error('Create post error:', error);
    } finally {
      setLoading(false);
    }
  };

  return (
    <div className="create-post">
      <h3>Create New Post</h3>
      <form onSubmit={handleSubmit}>
        <div className="form-group">
          <label>Title</label>
          <input
            type="text"
            value={title}
            onChange={(e) => setTitle(e.target.value)}
            required
          />
        </div>
        
        <div className="form-group">
          <label>Content</label>
          <textarea
            value={content}
            onChange={(e) => setContent(e.target.value)}
            required
            rows={5}
          />
        </div>
        
        <div className="form-group">
          <label>Tags (comma-separated)</label>
          <input
            type="text"
            value={tags}
            onChange={(e) => setTags(e.target.value)}
            placeholder="javascript, react, graphql"
          />
        </div>
        
        <div className="form-group">
          <label>Status</label>
          <select
            value={status}
            onChange={(e) => setStatus(e.target.value)}
          >
            <option value="DRAFT">Draft</option>
            <option value="PUBLISHED">Published</option>
            <option value="ARCHIVED">Archived</option>
          </select>
        </div>
        
        <button type="submit" disabled={loading}>
          {loading ? 'Creating...' : 'Create Post'}
        </button>
      </form>
    </div>
  );
};

export { PostList, PostItem, CreatePost };

Diseño de Esquema GraphQL

Sistema de Tipos

graph TD
    A[Schema] --> B[Types]
    A --> C[Queries]
    A --> D[Mutations]
    A --> E[Subscriptions]
    
    B --> F[Scalar Types]
    B --> G[Object Types]
    B --> H[Interface Types]
    B --> I[Union Types]
    B --> J[Enum Types]
    B --> K[Input Types]
    
    C --> L[Data Fetching]
    D --> M[Data Modification]
    E --> N[Real-time Updates]
    
    F --> O[String, Int, Float, Boolean, ID]
    G --> P[User, Post, Comment]
    H --> Q[Node, Entity]
    I --> R[SearchResult]
    J --> S[UserRole, PostStatus]
    K --> T[UserInput, PostInput]

GraphQL vs. REST

Tabla Comparativa

AspectoGraphQLREST
Obtención de DatosSolo datos necesariosEndpoints predefinidos
Cantidad de SolicitudesUna solicitud para todos los datosMúltiples solicitudes frecuentes
VersionadoSin versionado necesarioVersionado en URL
Seguridad de TiposTipado estrictoTipado débil
CachéMecanismo más complejoCaché HTTP simple
Manejo de ErroresErrores parciales posiblesCódigos HTTP de estado

Ventajas e Inconvenientes

Ventajas de GraphQL

  • Eficiencia: Trae solo los datos que necesitas
  • Flexibilidad: Una única consulta para datos complejos
  • Seguridad de Tipos: El tipado estricto previene errores
  • Evolución: El esquema crece gradualmente sin romper cambios
  • Tiempo Real: Las Subscriptions permiten actualizaciones instantáneas

Inconvenientes

  • Complejidad: Curva de aprendizaje más pronunciada que REST
  • Caché: El caché es más complejo que HTTP tradicional
  • Carga de Archivos: Las cargas de archivos requieren consideraciones adicionales
  • Limitación de Velocidad: La lógica de rate limiting se vuelve más intrincada
  • Observabilidad: Se necesitan herramientas especializadas

Preguntas Frecuentes: Esquema GraphQL y Componentes Principales

1. ¿Qué es GraphQL?

GraphQL es un lenguaje de consulta para APIs y un entorno de ejecución del lado del servidor. Permite que los clientes soliciten exactamente los datos que necesitan y define un esquema fuertemente tipado.

2. ¿Es GraphQL un framework?

No, GraphQL no es un software listo para usar ni un framework. Es una especificación y un lenguaje de consulta. Frameworks como Apollo Server implementan GraphQL en lenguajes de programación específicos.

3. ¿Qué es un esquema GraphQL?

El esquema es el contrato entre cliente y servidor. Describe mediante el lenguaje de definición de esquema qué tipos, queries, mutations y subscriptions están disponibles.

4. ¿Qué es el lenguaje de definición de esquema?

SDL es una sintaxis declarativa para definir esquemas GraphQL. Describe tipos, campos, relaciones y operaciones de forma clara y estructurada.

5. ¿Qué es una Query en GraphQL?

Una Query es una operación de lectura. Obtiene datos del servidor sin modificarlos. El cliente puede especificar exactamente qué campos desea recibir.

6. ¿Qué es una Mutation en GraphQL?

Una Mutation es una operación de escritura. Modifica datos en el servidor, como crear, actualizar o eliminar registros.

7. ¿Qué es una Subscription?

Una Subscription es una operación en tiempo real. El servidor notifica al cliente sobre eventos, típicamente a través de WebSockets, cuando los datos cambian.

8. ¿Qué es un Resolver?

Un Resolver es una función que proporciona datos para un campo específico en el esquema. Los Resolvers obtienen datos de bases de datos o APIs externas y pueden contener lógica de negocio.

9. ¿Qué son los tipos escalares en GraphQL?

Los tipos escalares son tipos de datos atómicos como String, Int, Float, Boolean e ID. Forman las hojas del árbol de consulta y no contienen subcampos adicionales.

10. ¿Qué es un Object Type?

Un Object Type es un tipo personalizado en el esquema que posee múltiples campos. Representa una entidad como Usuario o Publicación y puede estar vinculado con otros tipos.

11. ¿Qué es un Enum en GraphQL?

Un Enum es un tipo con una lista fija de valores permitidos. Se utiliza para definir de forma segura campos como estado, roles o categorías.

12. ¿Qué es una Interface en GraphQL?

Una Interface define un conjunto de campos que varios tipos deben implementar. Permite consultas polimórficas sobre diferentes tipos.

13. ¿Qué es una Union en GraphQL?

Una Union es un tipo que agrupa múltiples Object Types sin imponer campos comunes. En las consultas, el tipo concreto se diferencia usando Inline Fragments.

14. ¿Qué es un Input Type?

Un Input Type es un tipo especial para datos de entrada en Mutations. Se diferencia de los Object Types porque no tiene Resolvers y solo se usa para entradas.

15. ¿Qué es Apollo Server?

Apollo Server es un framework Node.js para APIs GraphQL. Se basa en un esquema y Resolvers, ofreciendo funcionalidades como autenticación, caché y Subscriptions.

16. ¿Qué es Apollo Client?

Apollo Client es una biblioteca JavaScript para acceder a APIs GraphQL. Cachea datos, gestiona estado y actualiza componentes de UI automáticamente cuando cambian los datos.

17. ¿Cuál es el problema N+1 en GraphQL?

El problema N+1 ocurre cuando se ejecuta una consulta a la base de datos separada para cada objeto en una lista. Data Loader agrupa estas solicitudes para mejorar el rendimiento.

18. ¿Qué es un Data Loader?

Un Data Loader es una utilidad que permite la carga en lotes. Recopila solicitudes en un corto período y las ejecuta en una única consulta a la base de datos.

19. ¿Qué es GraphQL Introspection?

Introspection permite que los clientes consulten el esquema en tiempo de ejecución. Herramientas como GraphQL Playground utilizan Introspection para ofrecer autocompletado y documentación.

20. ¿Qué es GraphQL Playground?

GraphQL Playground es un entorno de desarrollo interactivo para consultas GraphQL. Muestra el esquema, ofrece autocompletado y permite probar queries y mutations.

21. ¿Qué es Type Safety en GraphQL?

Type Safety significa que cada solicitud y resultado se validan contra el esquema. Esto reduce errores en tiempo de ejecución, ya que campos, tipos y argumentos se conocen de antemano.

22. ¿Qué es Schema Stitching?

Schema Stitching es la fusión de múltiples esquemas GraphQL en uno solo. Permite hacer disponibles APIs distribuidas de forma centralizada.

23. ¿Qué es Apollo Federation?

Apollo Federation es una arquitectura en la que múltiples microservicios proporcionan cada uno una parte del esquema total. Un gateway une estas partes en un esquema GraphQL unificado.

24. ¿Cuál es la diferencia entre GraphQL y REST?

GraphQL usa un único endpoint y permite selección precisa de campos. REST utiliza múltiples endpoints con recursos fijos. GraphQL ofrece tipado fuerte, mientras que REST se basa en métodos HTTP y códigos de estado.

25. ¿Cuándo se debe usar GraphQL?

GraphQL es especialmente adecuado para relaciones de datos complejas, clientes móviles con ancho de banda limitado y aplicaciones donde diferentes clientes necesitan vistas de datos distintas. Para APIs muy simples, REST puede ser suficiente.

Continuando en la ruta de aprendizaje de APIs

El próximo artículo en la ruta de aprendizaje de APIs cubre SOAP vs. REST vs. GraphQL — una comparación de los tres estilos de API más importantes con ventajas, desventajas y casos de uso.

Referencias principales

  1. https://graphql.org/
  2. https://www.apollographql.com/
  3. https://github.com/graphql/graphql-spec
  4. https://graphql-learn.com/
Volver al blog
Share:

Entradas relacionadas