Principios de diseño de schemas GraphQL
Un schema GraphQL bien diseñado es el contrato entre tu API y los consumidores. A diferencia de REST, donde el backend controla la estructura de las respuestas, en GraphQL el cliente pide exactamente lo que necesita. Esto exige pensar la modelación de datos desde las necesidades del frontend.
Empezá por los casos de uso: Antes de escribir types, listá las queries que los clientes necesitarán. 'Quiero mostrar un perfil con posts y seguidores' → necesitás type User con relaciones. Esto previene schemas con campos que nadie usa.
Tipado fuerte es clave: Usá ID! para identificadores obligatorios, String! para campos requeridos, [Type!]! para listas no-null de items no-null. El ! indica obligatoriedad. Esto captura errores en desarrollo, no en producción.
Relaciones bidireccionales: Si Post tiene author: User!, considerá si User necesita posts: [Post!]!. Pero ojo: relaciones bidireccionales pueden generar over-fetching si no usás DataLoader para batch queries y evitar N+1.
Evitar errores comunes de modelado
No calcar tu base de datos: Tu schema no es tu DB expuesta. Si tenés una tabla users_posts_likes con IDs foráneos, no crees un type UsersPostsLikes. Exponé Like con relaciones limpias. El schema debe modelar el dominio, no la implementación interna.
Nombres vagos o técnicos: type Data { info: String } no dice nada. Sé específico: type UserProfile { bio: String }. Evitá abreviaturas crípticas. El schema es documentación viva.
Queries con demasiados parámetros: users(filterByName: String, filterByEmail: String, filterByAge: Int, sortBy: String, order: String, limit: Int, offset: Int) es inmanejable. Usá input types: users(filter: UserFilter, pagination: PaginationInput).
Mutaciones sin input types: createUser(name: String! email: String! age: Int bio: String avatar: String) vs createUser(input: CreateUserInput!). Input types escalan mejor y permiten validaciones complejas.
Paginación, filtros y ordenamiento
Cursor-based pagination: Para feeds infinitos, usá conexiones: posts(first: Int after: String): PostConnection! con type PostConnection { edges: [PostEdge!]! pageInfo: PageInfo! }. Esto escala mejor que offset-based para datasets grandes.
Offset-based pagination: Para casos simples: users(limit: Int offset: Int): [User!]!. Funciona bien hasta ~10k records. Después, cursor-based es más eficiente.
Filtros complejos: Usá input types anidados. input ProductFilter { category: String priceRange: PriceRangeInput inStock: Boolean } con input PriceRangeInput { min: Float max: Float }. Esto permite composición de filtros sin explotar la firma de la query.
Ordenamiento: Enum para opciones: enum PostSort { RECENT POPULAR TRENDING }. Combinalo con order: SortOrder donde enum SortOrder { ASC DESC } si necesitás control de dirección.
Subscriptions y real-time
Subscriptions permiten push de datos desde el servidor. Usá WebSockets o Server-Sent Events según tu infraestructura. Estructura típica: type Subscription { messageAdded(conversationId: ID!): Message! }.
Cuándo usar subscriptions: Chat en tiempo real, notificaciones push, updates de estado (orden procesándose, stock cambiando), colaboración live (usuarios editando mismo doc). No para polling que puede ser query cada X segundos.
Performance: Cada subscription abierta consume recursos del servidor. Implementá autenticación en subscriptions: context debe validar que el user puede subscribirse al recurso. Limitá subscriptions por conexión para evitar abuse.
Alternativas: Si tu caso de uso tolera 5-10 segundos de latencia, polling con queries es más simple. Si necesitás live updates pero no sub-segundo, long-polling o webhooks pueden ser suficientes. Subscriptions agregan complejidad; validá que la justifican.