Crear Resolvers Anidados para Relaciones

Lectura
20 min~5 min lectura

Concepto clave

Los resolvers anidados son funciones que permiten resolver campos relacionados en GraphQL cuando una consulta solicita datos de múltiples niveles. Imagina que tienes una API para una biblioteca: cuando consultas un libro, también quieres información sobre su autor. En lugar de hacer dos consultas separadas, GraphQL te permite anidar estos datos en una sola petición.

En términos técnicos, cada campo en un tipo GraphQL puede tener su propio resolver. Cuando un campo representa una relación (como author en un tipo Book), necesitas un resolver anidado que obtenga esos datos relacionados. Esto es fundamental para construir APIs eficientes y tipadas, ya que evita el over-fetching (obtener datos innecesarios) y permite estructuras de datos complejas.

Cómo funciona en la práctica

Supongamos que tienes un esquema GraphQL con tipos Book y Author. El tipo Book tiene un campo author que referencia un Author. Para implementar esto en Apollo Server con TypeScript:

type Book {
  id: ID!
  title: String!
  author: Author!
}

type Author {
  id: ID!
  name: String!
  books: [Book!]!
}

Los resolvers se estructuran así:

const resolvers = {
  Book: {
    author: async (parent, args, context) => {
      // parent contiene los datos del libro actual
      return await context.dataSources.authors.getById(parent.authorId);
    }
  },
  Author: {
    books: async (parent, args, context) => {
      return await context.dataSources.books.getByAuthorId(parent.id);
    }
  }
};

El parent en el resolver de author es el objeto Book actual, que típicamente incluye un authorId para buscar el autor relacionado. Apollo Server ejecuta estos resolvers automáticamente cuando se solicita el campo anidado.

Caso de estudio

Vamos a aplicar esto a un sistema de e-commerce con Next.js y Apollo Server. Tenemos tipos para Order y Product, donde cada orden contiene productos. El esquema:

type Order {
  id: ID!
  date: String!
  products: [Product!]!
}

type Product {
  id: ID!
  name: String!
  price: Float!
}

En la base de datos, las órdenes y productos se almacenan en tablas separadas, con una tabla de unión order_products. El resolver para products en Order:

const resolvers = {
  Order: {
    products: async (parent, args, context) => {
      // parent.id es el ID de la orden
      const productIds = await context.db.orderProducts.findMany({
        where: { orderId: parent.id },
        select: { productId: true }
      });
      return await context.db.products.findMany({
        where: { id: { in: productIds.map(p => p.productId) } }
      });
    }
  }
};

Este enfoque permite consultas como:

query {
  order(id: "123") {
    date
    products {
      name
      price
    }
  }
}

Obteniendo todos los datos en una sola petición, optimizando el rendimiento.

Errores comunes

  • Resolver infinito: Si Book tiene un campo author y Author tiene un campo books, y ambos se resuelven mutuamente sin límite, puede causar un bucle infinito. Solución: Limitar la profundidad de la consulta o usar dataloader para caching.
  • N+1 queries: En el caso de estudio, si consultas 10 órdenes, el resolver de products se ejecuta 10 veces, generando muchas consultas a la base de datos. Evítalo con dataloader para agrupar peticiones.
  • Tipado incorrecto: No definir bien los tipos en TypeScript puede llevar a errores en tiempo de ejecución. Asegúrate de que los resolvers devuelvan el tipo exacto esperado por el esquema.
  • Olvidar el contexto: No pasar el context correctamente en Apollo Server puede hacer que los resolvers no accedan a fuentes de datos. Verifica la configuración del servidor.

Checklist de dominio

  1. Entiendo cómo los resolvers anidados mapean campos GraphQL a funciones en Apollo Server.
  2. Puedo implementar un resolver anidado para una relación uno-a-muchos (ej: autor-libros).
  3. Sé usar el parámetro parent para acceder a datos del objeto actual en el resolver.
  4. Reconozco y evito problemas de N+1 queries usando técnicas como dataloader.
  5. Verifico que los tipos TypeScript en los resolvers coincidan con el esquema GraphQL.
  6. Puedo debuggear resolvers anidados usando herramientas como Apollo Studio.
  7. Aplico esto en un proyecto real con Next.js para construir APIs tipadas.

Implementar Resolvers Anidados para un Blog

En este ejercicio, crearás resolvers anidados para un sistema de blog usando Next.js y Apollo Server. El blog tiene tipos Post y Comment, donde cada post puede tener múltiples comentarios.

  1. Define un esquema GraphQL en un archivo schema.ts con tipos Post (campos: id, title, content, comments) y Comment (campos: id, text, postId). Asegúrate de que Post.comments sea una lista de Comment.
  2. Configura Apollo Server en un archivo pages/api/graphql.ts en Next.js, incluyendo los tipos definidos.
  3. Implementa resolvers en un archivo resolvers.ts. Crea un resolver anidado para el campo comments en Post que, dado el id del post (disponible en parent.id), busque los comentarios relacionados desde una fuente de datos simulada (puedes usar un array en memoria).
  4. Escribe una consulta GraphQL en Apollo Studio o un cliente para probar que, al solicitar un post, también obtienes sus comentarios anidados.
  5. Tipa los resolvers con TypeScript, asegurando que los tipos de retorno coincidan con el esquema.
Pistas
  • Usa el parámetro parent en el resolver para acceder al id del post actual.
  • Simula la fuente de datos con un array de objetos que incluya postId en los comentarios.
  • Verifica que la consulta GraphQL solicite explícitamente el campo comments dentro de post.

Evalua tu comprension

Completa el quiz interactivo de arriba para ganar XP.