InterviewHack.ai
Empezar gratis
Blog/Preguntas de entrevista de GraphQL — 35 con código y respuestas

Preguntas de entrevista de GraphQL — 35 con código y respuestas

16 de septiembre de 2026

graphqlbackend-developer

35 preguntas de GraphQL para entrevistas backend y full-stack: queries, mutations, subscriptions, resolvers, DataLoader, N+1 problem, schema design. Con código.

Preguntas de entrevista de GraphQL — 35 con código y respuestas

Si te preparás para una entrevista backend o full-stack y mencionás GraphQL en tu CV, esperá que el entrevistador vaya fondo. No alcanza con saber que "es una alternativa a REST" — necesitás entender el modelo de ejecución, los patrones reales de producción y dónde duele. Esta guía cubre 35 preguntas con respuestas completas y código en Apollo Server, Node.js y TypeScript.


Fundamentos: REST vs GraphQL

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

REST expone múltiples endpoints, cada uno representando un recurso. El servidor decide qué datos devuelve. GraphQL expone un único endpoint y el cliente especifica exactamente qué campos necesita.

Esto resuelve dos problemas clásicos de REST:

  • Over-fetching: recibís más datos de los que necesitás (una respuesta /users/:id que devuelve 40 campos cuando solo necesitás el nombre).
  • Under-fetching: necesitás hacer múltiples requests para ensamblar la vista (/users/:id, luego /users/:id/posts, luego /posts/:id/comments).
typescript
// REST: tres llamadas para mostrar un perfil
GET /users/42
GET /users/42/posts
GET /posts/7/comments

// GraphQL: una sola query
query {
  user(id: "42") {
    name
    email
    posts {
      title
      comments {
        body
        author { name }
      }
    }
  }
}

Lo que busca el entrevistador: que entiendas el problema real que resuelve GraphQL, no solo la definición de libro. Mencioná over-fetching y under-fetching; son las palabras clave que esperan escuchar.


2. ¿En qué casos seguirías usando REST en lugar de GraphQL?

GraphQL no es la bala de plata. Seguirías con REST cuando:

  • La API es pública y simple (payloads bien definidos, sin necesidad de flexibilidad en el cliente).
  • Necesitás caché HTTP nativo sin complejidad adicional — REST usa métodos GET con URLs cacheables directamente en CDNs; GraphQL sobre HTTP POST no tiene eso out of the box.
  • El equipo es chico y el overhead de schemas, resolvers y el tooling no justifica el costo.
  • El cliente es siempre el mismo (ej: una API interna que consume un solo servicio).

REST también gana en uploads de archivos — GraphQL no tiene soporte nativo para multipart y requiere workarounds como graphql-upload.


3. ¿Qué es el SDL (Schema Definition Language)?

El SDL es el lenguaje de texto que usás para definir el schema de GraphQL — los tipos, sus campos, las relaciones y las operaciones disponibles.

graphql
type User {
  id: ID!
  name: String!
  email: String!
  posts: [Post!]!
  createdAt: String!
}

type Post {
  id: ID!
  title: String!
  body: String
  author: User!
}

type Query {
  user(id: ID!): User
  posts: [Post!]!
}

type Mutation {
  createPost(title: String!, body: String!): Post!
}

El ! marca un campo como non-null. [Post!]! significa: un array no nulo que solo contiene elementos no nulos.


Queries y Mutations

4. ¿Qué diferencia hay entre una Query y una Mutation?

Semánticamente, una Query es de solo lectura y una Mutation modifica datos. GraphQL garantiza que las Queries pueden ejecutarse en paralelo; las Mutations se ejecutan en serie (una tras otra en el orden en que las escribís).

graphql
# Query — lectura
query GetUser($id: ID!) {
  user(id: $id) {
    name
    email
  }
}

# Mutation — escritura
mutation CreatePost($title: String!, $body: String!) {
  createPost(title: $title, body: $body) {
      id
      title
      createdAt
  }
}

En el servidor con Apollo:

typescript
const resolvers = {
  Query: {
    user: async (_: unknown, { id }: { id: string }, context: Context) => {
      return context.db.users.findById(id);
    },
  },
  Mutation: {
    createPost: async (
      _: unknown,
      { title, body }: { title: string; body: string },
      context: Context
    ) => {
      if (!context.currentUser) throw new AuthenticationError("No autenticado");
      return context.db.posts.create({
        title,
        body,
        authorId: context.currentUser.id,
      });
    },
  },
};

5. ¿Qué son los fragments y para qué los usás?

Los fragments son piezas reutilizables de una query. Te evitan repetir la selección de campos cuando la misma estructura aparece en múltiples lugares.

graphql
fragment UserBasic on User {
  id
  name
  email
}

query GetUsers {
  admins {
    ...UserBasic
    role
  }
  editors {
    ...UserBasic
    department
  }
}

En el front, los frameworks como Relay los usan mucho para colocar las necesidades de datos junto al componente que los consume (co-location).


6. ¿Qué son las variables en GraphQL y por qué son importantes para la seguridad?

Las variables permiten parametrizar las operaciones. En vez de interpolar valores directamente en la query string (lo que abriría la puerta a injection), se pasan por separado.

graphql
# ❌ Mal — interpolación manual (nunca hagás esto)
query {
  user(id: "${userId}") { name }
}

# ✅ Bien — variable tipada
query GetUser($id: ID!) {
  user(id: $id) { name }
}

Variables en la llamada HTTP:

json
{
  "query": "query GetUser($id: ID!) { user(id: $id) { name } }",
  "variables": { "id": "42" }
}

Seguridad: GraphQL valida las variables contra el tipo definido en el schema antes de ejecutar los resolvers. No podés pasar un objeto malicioso donde se espera un ID.


7. ¿Qué son los aliases en GraphQL?

Los aliases te permiten renombrar el resultado de un campo en la respuesta. Son útiles cuando necesitás llamar al mismo campo dos veces con distintos argumentos.

graphql
query ComparePosts {
  published: posts(status: PUBLISHED) {
    id
    title
  }
  drafts: posts(status: DRAFT) {
    id
    title
  }
}

Sin aliases, el servidor devolvería dos claves posts y la segunda pijaría a la primera.


Subscriptions en tiempo real

8. ¿Cómo funcionan las Subscriptions en GraphQL?

Las Subscriptions son operaciones de larga duración. En lugar del ciclo request/response, el cliente establece una conexión persistente (generalmente WebSocket) y el servidor pushea actualizaciones cada vez que ocurre un evento relevante.

graphql
subscription OnNewMessage($channelId: ID!) {
  messageSent(channelId: $channelId) {
    id
    body
    sender { name }
    createdAt
  }
}

En Apollo Server con graphql-ws:

typescript
import { createServer } from "http";
import { makeExecutableSchema } from "@graphql-tools/schema";
import { WebSocketServer } from "ws";
import { useServer } from "graphql-ws/lib/use/ws";
import { PubSub } from "graphql-subscriptions";

const pubsub = new PubSub();
const MESSAGE_SENT = "MESSAGE_SENT";

const resolvers = {
  Mutation: {
    sendMessage: async (_: unknown, { channelId, body }: any, ctx: Context) => {
      const message = await ctx.db.messages.create({ channelId, body, senderId: ctx.currentUser.id });
      await pubsub.publish(MESSAGE_SENT, { messageSent: message, channelId });
      return message;
    },
  },
  Subscription: {
    messageSent: {
      subscribe: (_: unknown, { channelId }: { channelId: string }) =>
        pubsub.asyncIterator([MESSAGE_SENT]),
      resolve: (payload: any) => payload.messageSent,
    },
  },
};

Trampa común: PubSub de graphql-subscriptions es in-memory, solo funciona con una instancia del proceso. En producción necesitás graphql-redis-subscriptions o un broker externo para deployments con múltiples pods.


9. ¿Cuándo usarías una Subscription vs polling?

| Criterio | Subscription | Polling |

|---|---|---|

| Frecuencia de eventos | Alta o impredecible | Baja y predecible |

| Latencia requerida | Baja (tiempo real) | Tolerable (segundos) |

| Complejidad infra | Mayor (WebSockets, broker) | Menor |

| Escalabilidad | Requiere sticky sessions o pub/sub | Stateless, escala fácil |

Para un chat o un tablero de precios en tiempo real → Subscription. Para "refrescar cada 30 segundos si hay notificaciones" → polling con una Query ordinaria.


Diseño de schema: Types, Interfaces y Unions

10. ¿Cuál es la diferencia entre Interface y Union en GraphQL?

  • Una Interface define un contrato de campos que todos los tipos que la implementan deben tener.
  • Una Union agrupa tipos que no necesariamente comparten campos.
graphql
# Interface — todos los tipos tienen id y createdAt
interface Node {
  id: ID!
  createdAt: String!
}

type User implements Node {
  id: ID!
  createdAt: String!
  email: String!
}

type Post implements Node {
  id: ID!
  createdAt: String!
  title: String!
}

# Union — tipos sin campos en común
union SearchResult = User | Post | Comment

type Query {
  node(id: ID!): Node
  search(query: String!): [SearchResult!]!
}

Al consultar una Union necesitás inline fragments para acceder a campos específicos:

graphql
query Search($q: String!) {
  search(query: $q) {
    __typename
    ... on User { name email }
    ... on Post { title body }
    ... on Comment { body author { name } }
  }
}

11. ¿Qué son los Input Types y por qué no reutilizás los tipos regulares para argumentos?

Los Input Types son versiones de los tipos de datos pensadas específicamente para usarse como argumentos de mutations. Los tipos regulares pueden tener resolvers, referencias circulares complejas o campos que no tiene sentido recibir del cliente (como id en una creación).

graphql
input CreateUserInput {
  name: String!
  email: String!
  password: String!
}

input UpdateUserInput {
  name: String
  email: String
}

type Mutation {
  createUser(input: CreateUserInput!): User!
  updateUser(id: ID!, input: UpdateUserInput!): User!
}

Error común en entrevistas: decir que podés usar un type donde va un input. GraphQL no lo permite — los tipos de output y de input son mundos separados.


12. ¿Qué son los Enums y los Scalars personalizados?

Enums definen un conjunto cerrado de valores válidos:

graphql
enum PostStatus {
  DRAFT
  PUBLISHED
  ARCHIVED
}

type Post {
  status: PostStatus!
}

Scalars personalizados permiten tipos que GraphQL no tiene por defecto (Date, Email, URL, JSON):

typescript
import { GraphQLScalarType, Kind } from "graphql";

const DateScalar = new GraphQLScalarType({
  name: "Date",
  description: "Fecha en formato ISO 8601",
  serialize(value: unknown) {
    if (value instanceof Date) return value.toISOString();
    throw new Error("DateScalar solo serializa objetos Date");
  },
  parseValue(value: unknown) {
    if (typeof value === "string") return new Date(value);
    throw new Error("DateScalar espera un string");
  },
  parseLiteral(ast) {
    if (ast.kind === Kind.STRING) return new Date(ast.value);
    throw new Error("DateScalar espera un string literal");
  },
});

Resolvers y Context

13. ¿Qué son los cuatro argumentos de un resolver?

Todo resolver recibe exactamente cuatro argumentos:

typescript
type Resolver<Parent = unknown, Args = Record<string, unknown>, Context = unknown, Return = unknown> = (
  parent: Parent,   // resultado del resolver padre
  args: Args,       // argumentos de la query/mutation
  context: Context, // objeto compartido (db, usuario autenticado, etc.)
  info: GraphQLResolveInfo // metadata del AST de la query
) => Return | Promise<Return>;

Ejemplo práctico:

typescript
const resolvers = {
  User: {
    // parent = objeto User del resolver padre
    posts: async (parent: User, _args: unknown, ctx: Context) => {
      return ctx.loaders.postsByAuthor.load(parent.id);
    },
  },
  Query: {
    // parent = undefined (resolver raíz)
    user: async (_parent: unknown, { id }: { id: string }, ctx: Context) => {
      return ctx.db.users.findById(id);
    },
  },
};

14. ¿Cómo construís el Context y qué va ahí?

El context es el único mecanismo correcto para inyectar dependencias compartidas entre resolvers sin globals. Va ahí:

  • La conexión a base de datos o el ORM
  • El usuario autenticado (decodificado del JWT)
  • DataLoaders (un DataLoader por request)
  • Loggers, tracers
typescript
import { ApolloServer } from "@apollo/server";
import { expressMiddleware } from "@apollo/server/express4";
import jwt from "jsonwebtoken";

interface Context {
  currentUser: User | null;
  db: Database;
  loaders: ReturnType<typeof createLoaders>;
}

const server = new ApolloServer<Context>({ typeDefs, resolvers });

app.use(
  "/graphql",
  expressMiddleware(server, {
    context: async ({ req }): Promise<Context> => {
      const token = req.headers.authorization?.replace("Bearer ", "");
      let currentUser: User | null = null;
      if (token) {
        try {
          const payload = jwt.verify(token, process.env.JWT_SECRET!) as { userId: string };
          currentUser = await db.users.findById(payload.userId);
        } catch {
          // token inválido — no tiramos error, resolvemos como anónimo
        }
      }
      return {
        currentUser,
        db,
        loaders: createLoaders(db), // nuevo DataLoader por request
      };
    },
  })
);

Importante: los DataLoaders se crean frescos en cada request. Si los reutilizás entre requests, el caché interno va a devolver datos stale.


15. ¿Qué son los resolvers de campo y cuándo los necesitás?

GraphQL tiene un resolver por defecto que simplemente devuelve parent[fieldName]. Los resolvers de campo (field resolvers) los escribís solo cuando la lógica es más compleja:

typescript
const resolvers = {
  User: {
    // Campo derivado — no existe en DB, lo calculamos
    fullName: (parent: User) => `${parent.firstName} ${parent.lastName}`,

    // Campo que requiere una query separada
    posts: async (parent: User, _: unknown, ctx: Context) => {
      return ctx.loaders.postsByAuthor.load(parent.id);
    },

    // Transformación de formato
    createdAt: (parent: User) => new Date(parent.createdAt).toISOString(),
  },
};

El problema N+1 y DataLoader

16. Explicá el problema N+1 en GraphQL

El N+1 es el bug de rendimiento más típico en GraphQL. Ocurre cuando resolvés una lista y cada elemento dispara una query adicional a la base de datos.

typescript
// Schema:
// posts: [Post!]!
// Post.author: User!

// Query del cliente:
// { posts { title author { name } } }

const resolvers = {
  Query: {
    posts: () => db.posts.findAll(), // 1 query → devuelve 100 posts
  },
  Post: {
    // ❌ Este resolver se ejecuta 100 veces, uno por post
    author: (parent: Post) => db.users.findById(parent.authorId), // 100 queries
  },
};
// Total: 1 + 100 = 101 queries para mostrar 100 posts

Con 100 posts tenés 101 queries. Con 1000, tenés 1001. La base de datos explota.


17. ¿Cómo resolvés el N+1 con DataLoader?

DataLoader agrupa (batch) múltiples llamadas que ocurren en el mismo tick del event loop y las ejecuta en una sola query.

typescript
import DataLoader from "dataloader";

// Función batch: recibe un array de IDs, devuelve un array de resultados en el mismo orden
async function batchUsers(ids: readonly string[]): Promise<User[]> {
  const users = await db.users.findByIds([...ids]);
  // DataLoader exige que el resultado esté en el mismo orden que los IDs
  const userMap = new Map(users.map((u) => [u.id, u]));
  return ids.map((id) => userMap.get(id) ?? new Error(`User ${id} not found`));
}

// Creás el loader por request (en el context factory)
function createLoaders(db: Database) {
  return {
    users: new DataLoader<string, User>(batchUsers),
  };
}

// En el resolver:
const resolvers = {
  Post: {
    // ✅ DataLoader agrupa los 100 `load(authorId)` en una sola query
    author: (parent: Post, _: unknown, ctx: Context) => {
      return ctx.loaders.users.load(parent.authorId);
    },
  },
};
// Total: 2 queries (1 para posts, 1 para todos los autores)

DataLoader también tiene caché por request: si el mismo ID se pide dos veces, se devuelve el resultado cacheado sin ir a la DB.


18. ¿Cuándo DataLoader no alcanza y qué más podés hacer?

DataLoader resuelve el N+1 para relaciones uno-a-uno o muchos-a-uno. Para relaciones uno-a-muchos o muchos-a-muchos el batch function puede volverse complejo. Otras estrategias:

  • Join Monster o Prisma: generan JOINs SQL inteligentes basados en el campo info del resolver.
  • Persisted Queries: evitás queries ad-hoc en producción; ejecutás solo queries pre-autorizadas.
  • Query complexity limits: limitás el depth o la complejidad del grafo para prevenir queries abusivas.
typescript
import { createComplexityRule, fieldExtensionsEstimator, simpleEstimator } from "graphql-query-complexity";
import { validate } from "graphql";

const complexityRule = createComplexityRule({
  maximumComplexity: 1000,
  variables: {},
  onComplete: (complexity: number) => console.log("Query complexity:", complexity),
  estimators: [
    fieldExtensionsEstimator(),
    simpleEstimator({ defaultComplexity: 1 }),
  ],
});

Paginación

19. ¿Cuál es la diferencia entre paginación offset y cursor-based?

Offset-based (LIMIT/OFFSET en SQL):

graphql
query {
  posts(offset: 20, limit: 10) {
    id
    title
  }
}

Simple de implementar pero tiene problemas:

  • Si se inserta un item entre la página 2 y la 3, el usuario ve duplicados o se saltea items.
  • Escala mal en tablas grandes (la DB tiene que contar todos los registros previos).

Cursor-based (Relay spec):

graphql
query {
  posts(first: 10, after: "cursor_opaco_base64") {
    edges {
      cursor
      node {
        id
        title
      }
    }
    pageInfo {
      hasNextPage
      endCursor
    }
  }
}

El cursor apunta a un registro específico (generalmente el ID o un timestamp). No hay problema con inserciones concurrentes y escala bien.


20. Implementá cursor-based pagination en TypeScript

typescript
function encodeCursor(id: string): string {
  return Buffer.from(`cursor:${id}`).toString("base64");
}

function decodeCursor(cursor: string): string {
  return Buffer.from(cursor, "base64").toString("utf8").replace("cursor:", "");
}

const resolvers = {
  Query: {
    posts: async (
      _: unknown,
      { first = 10, after }: { first?: number; after?: string },
      ctx: Context
    ) => {
      const afterId = after ? decodeCursor(after) : null;

      const posts = await ctx.db.posts.findMany({
        take: first + 1, // pedimos uno extra para saber si hay siguiente página
        cursor: afterId ? { id: afterId } : undefined,
        skip: afterId ? 1 : 0,
        orderBy: { createdAt: "desc" },
      });

      const hasNextPage = posts.length > first;
      const edges = posts.slice(0, first).map((post) => ({
        cursor: encodeCursor(post.id),
        node: post,
      }));

      return {
        edges,
        pageInfo: {
          hasNextPage,
          endCursor: edges.length > 0 ? edges[edges.length - 1].cursor : null,
        },
      };
    },
  },
};

Autenticación y Autorización

21. ¿Cuál es la diferencia entre autenticación y autorización en GraphQL?

  • Autenticación: ¿quién sos? Se valida en el context factory, antes de ejecutar ningún resolver.
  • Autorización: ¿podés hacer esto? Se implementa dentro de los resolvers o en una capa de directivas.
typescript
// Autenticación — en el context factory (ya lo vimos arriba)
// Si el token es inválido, currentUser es null

// Autorización — en el resolver
const resolvers = {
  Mutation: {
    deletePost: async (_: unknown, { id }: { id: string }, ctx: Context) => {
      if (!ctx.currentUser) {
        throw new GraphQLError("No autenticado", {
          extensions: { code: "UNAUTHENTICATED" },
        });
      }
      const post = await ctx.db.posts.findById(id);
      if (!post) throw new GraphQLError("Post no encontrado", { extensions: { code: "NOT_FOUND" } });
      if (post.authorId !== ctx.currentUser.id && ctx.currentUser.role !== "ADMIN") {
        throw new GraphQLError("Sin permisos para eliminar este post", {
          extensions: { code: "FORBIDDEN" },
        });
      }
      return ctx.db.posts.delete(id);
    },
  },
};

22. ¿Qué son las Schema Directives y cómo las usás para autorización?

Las directivas son anotaciones en el schema que modifican el comportamiento de la ejecución. Una directiva @auth es el patrón más común para centralizar la autorización:

graphql
directive @auth(requires: Role = USER) on FIELD_DEFINITION

enum Role {
  USER
  ADMIN
}

type Mutation {
  deletePost(id: ID!): Boolean! @auth(requires: ADMIN)
  createPost(input: CreatePostInput!): Post! @auth
}
typescript
import { mapSchema, getDirective, MapperKind } from "@graphql-tools/utils";
import { defaultFieldResolver } from "graphql";

function authDirectiveTransformer(schema: GraphQLSchema) {
  return mapSchema(schema, {
    [MapperKind.OBJECT_FIELD]: (fieldConfig) => {
      const authDirective = getDirective(schema, fieldConfig, "auth")?.[0];
      if (!authDirective) return fieldConfig;

      const { requires = "USER" } = authDirective;
      const { resolve = defaultFieldResolver } = fieldConfig;

      return {
        ...fieldConfig,
        resolve: async (source, args, context: Context, info) => {
          if (!context.currentUser) {
            throw new GraphQLError("No autenticado", {
              extensions: { code: "UNAUTHENTICATED" },
            });
          }
          if (requires === "ADMIN" && context.currentUser.role !== "ADMIN") {
            throw new GraphQLError("Acceso denegado", {
              extensions: { code: "FORBIDDEN" },
            });
          }
          return resolve(source, args, context, info);
        },
      };
    },
  });
}

23. ¿Cómo protegés una API GraphQL de ataques de complejidad o profundidad excesiva?

Un cliente malicioso podría enviar una query con profundidad infinita:

graphql
# Query de "death star" — amigos de amigos infinitos
{
  user(id: "1") {
    friends {
      friends {
        friends {
          friends { ... }
        }
      }
    }
  }
}

Para protegerte:

typescript
import depthLimit from "graphql-depth-limit";
import { createComplexityRule } from "graphql-query-complexity";

const server = new ApolloServer({
  typeDefs,
  resolvers,
  validationRules: [
    depthLimit(7), // máximo 7 niveles de profundidad
    createComplexityRule({
      maximumComplexity: 500,
      estimators: [simpleEstimator({ defaultComplexity: 1 })],
    }),
  ],
});

Error Handling

24. ¿Cómo maneja GraphQL los errores y cómo los exponés al cliente?

GraphQL tiene una diferencia clave con REST: siempre devuelve HTTP 200. Los errores van en el campo errors de la respuesta:

json
{
  "data": { "user": null },
  "errors": [
    {
      "message": "Usuario no encontrado",
      "locations": [{ "line": 2, "column": 3 }],
      "path": ["user"],
      "extensions": { "code": "NOT_FOUND" }
    }
  ]
}

En Apollo Server v4:

typescript
import { GraphQLError } from "graphql";

const resolvers = {
  Query: {
    user: async (_: unknown, { id }: { id: string }, ctx: Context) => {
      const user = await ctx.db.users.findById(id);
      if (!user) {
        throw new GraphQLError("Usuario no encontrado", {
          extensions: {
            code: "NOT_FOUND",
            http: { status: 404 }, // Apollo v4 respeta esto para el status HTTP
          },
        });
      }
      return user;
    },
  },
};

Formateo global de errores (importante para no exponer stacktraces en producción):

typescript
const server = new ApolloServer({
  typeDefs,
  resolvers,
  formatError: (formattedError, error) => {
    // En producción: no expongas el stack
    if (process.env.NODE_ENV === "production") {
      return {
        message: formattedError.message,
        extensions: { code: formattedError.extensions?.code ?? "INTERNAL_ERROR" },
      };
    }
    return formattedError;
  },
});

25. ¿Qué es el patrón de "errores como valores" en el schema?

En lugar de lanzar errores, algunos equipos modelan las respuestas de mutations como unions que incluyen el tipo de error:

graphql
type CreatePostSuccess {
  post: Post!
}

type ValidationError {
  field: String!
  message: String!
}

union CreatePostResult = CreatePostSuccess | ValidationError

type Mutation {
  createPost(input: CreatePostInput!): CreatePostResult!
}
typescript
const resolvers = {
  Mutation: {
    createPost: async (_: unknown, { input }: any, ctx: Context) => {
      if (!input.title || input.title.length < 3) {
        return {
          __typename: "ValidationError",
          field: "title",
          message: "El título necesita al menos 3 caracteres",
        };
      }
      const post = await ctx.db.posts.create(input);
      return { __typename: "CreatePostSuccess", post };
    },
  },
};

Ventaja: el cliente sabe en el tipo si la operación tuvo éxito o no, sin parsear el campo errors. Desventaja: complejiza el schema. Usalo para errores de negocio esperados, no para errores de infraestructura.


Testing de Resolvers

26. ¿Cómo testeás resolvers unitariamente?

Los resolvers son funciones puras que reciben argumentos y devuelven datos. Se testean directamente sin levantar el servidor:

typescript
import { describe, it, expect, vi } from "vitest";
import { resolvers } from "./resolvers";

describe("Query.user", () => {
  it("devuelve el usuario cuando existe", async () => {
    const mockUser = { id: "1", name: "Ana García", email: "ana@test.com" };
    const mockDb = {
      users: { findById: vi.fn().mockResolvedValue(mockUser) },
    };
    const context = { db: mockDb, currentUser: null, loaders: {} };

    const result = await resolvers.Query.user(undefined, { id: "1" }, context as any, {} as any);

    expect(result).toEqual(mockUser);
    expect(mockDb.users.findById).toHaveBeenCalledWith("1");
  });

  it("lanza error cuando no existe el usuario", async () => {
    const mockDb = { users: { findById: vi.fn().mockResolvedValue(null) } };
    const context = { db: mockDb, currentUser: null, loaders: {} };

    await expect(
      resolvers.Query.user(undefined, { id: "999" }, context as any, {} as any)
    ).rejects.toThrow("Usuario no encontrado");
  });
});

27. ¿Cómo testeás la API completa de extremo a extremo?

Para tests de integración, @apollo/server tiene soporte para ejecutar operaciones sin HTTP:

typescript
import { ApolloServer } from "@apollo/server";
import { typeDefs } from "./schema";
import { resolvers } from "./resolvers";

describe("Integration: createPost mutation", () => {
  let server: ApolloServer;

  beforeAll(async () => {
    server = new ApolloServer({ typeDefs, resolvers });
    await server.start();
  });

  afterAll(async () => {
    await server.stop();
  });

  it("crea un post cuando el usuario está autenticado", async () => {
    const CREATE_POST = `
      mutation CreatePost($title: String!, $body: String!) {
        createPost(title: $title, body: $body) {
          id
          title
        }
      }
    `;

    const response = await server.executeOperation(
      {
        query: CREATE_POST,
        variables: { title: "Test post", body: "Contenido de prueba" },
      },
      {
        contextValue: {
          currentUser: { id: "user-1", role: "USER" },
          db: mockDb,
          loaders: createLoaders(mockDb),
        },
      }
    );

    expect(response.body.kind).toBe("single");
    if (response.body.kind === "single") {
      expect(response.body.singleResult.errors).toBeUndefined();
      expect(response.body.singleResult.data?.createPost).toHaveProperty("title", "Test post");
    }
  });
});

28. ¿Cómo testeás que los resolvers requieren autenticación?

typescript
it("rechaza la mutation sin autenticación", async () => {
  const response = await server.executeOperation(
    { query: CREATE_POST, variables: { title: "Test", body: "Body" } },
    { contextValue: { currentUser: null, db: mockDb, loaders: createLoaders(mockDb) } }
  );

  if (response.body.kind === "single") {
    const errors = response.body.singleResult.errors;
    expect(errors).toBeDefined();
    expect(errors?.[0].extensions?.code).toBe("UNAUTHENTICATED");
  }
});

Federation

29. ¿Qué es Apollo Federation y para qué sirve?

Apollo Federation permite dividir un schema GraphQL grande en múltiples subgraphs, cada uno mantenido por un equipo distinto. Un Router (antes Gateway) compone todos los subgraphs en una API unificada que el cliente ve como un solo schema.

Cliente → Apollo Router → Subgraph Users (Node.js)
                       → Subgraph Products (Go)
                       → Subgraph Orders (Python)

Es la solución de GraphQL para arquitecturas de microservicios.


30. ¿Cómo se define una entidad en Federation?

Las entities son tipos que varios subgraphs pueden extender. Se definen con @key:

typescript
// Subgraph: users-service
import { buildSubgraphSchema } from "@apollo/subgraph";
import gql from "graphql-tag";

const typeDefs = gql`
  extend schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key"])

  type User @key(fields: "id") {
    id: ID!
    name: String!
    email: String!
  }
`;

const resolvers = {
  User: {
    __resolveReference: async (reference: { id: string }, ctx: Context) => {
      return ctx.db.users.findById(reference.id);
    },
  },
};

export const schema = buildSubgraphSchema({ typeDefs, resolvers });
typescript
// Subgraph: orders-service
const typeDefs = gql`
  extend schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key", "@external"])

  type User @key(fields: "id") {
    id: ID! @external
    orders: [Order!]!
  }

  type Order {
    id: ID!
    total: Float!
    status: String!
  }
`;

El Router une ambos subgraphs. Cuando el cliente pide user { name orders { total } }, el Router hace un plan: primero llama a users-service para el nombre, luego a orders-service con el id del usuario para las órdenes.


31. ¿Qué son las directivas `@requires` y `@provides` en Federation?

  • @requires: le dice al Router que un campo necesita campos del subgraph base antes de resolverse.
  • @provides: le dice al Router que este subgraph puede devolver campos de otra entidad, evitando un fetch adicional.
graphql
# En el subgraph de productos
type Product @key(fields: "id") {
  id: ID!
  name: String!
  price: Float!
}

# En el subgraph de reseñas
type Review @key(fields: "id") {
  id: ID!
  product: Product @provides(fields: "name")
  rating: Int!
}

# product.name ya viene con las reseñas — no hace falta llamar al subgraph de productos

Preguntas avanzadas y de situación real

32. ¿Cómo implementarías rate limiting en GraphQL?

A diferencia de REST, no podés limitar por endpoint. Limitás por operación o por complejidad:

typescript
import { RateLimiterMemory } from "rate-limiter-flexible";

const rateLimiter = new RateLimiterMemory({
  points: 100,   // 100 operaciones
  duration: 60,  // por minuto
});

// En el context factory:
context: async ({ req }) => {
  const ip = req.ip ?? "unknown";
  try {
    await rateLimiter.consume(ip);
  } catch {
    throw new GraphQLError("Demasiadas requests, intentá en un momento", {
      extensions: { code: "RATE_LIMITED", http: { status: 429 } },
    });
  }
  // ...resto del context
}

Para límites más granulares por operación, podés combinar con graphql-query-complexity.


33. ¿Qué es el caching en GraphQL y por qué es más complejo que en REST?

En REST, el caché HTTP funciona con URLs + métodos GET. En GraphQL todas las queries van al mismo endpoint (POST /graphql), así que el caché HTTP nativo no aplica.

Estrategias:

  1. 1Persisted Queries / APQ (Automatic Persisted Queries): el cliente envía un hash de la query; si el servidor la reconoce, ejecuta sin recibir el documento completo. Reduce el payload y permite caché en CDN por hash.
  1. 2Response caching (plugin de Apollo): caché a nivel de resultado de operación:
typescript
import responseCachePlugin from "@apollo/server-plugin-response-cache";

const server = new ApolloServer({
  typeDefs,
  resolvers,
  plugins: [responseCachePlugin()],
});

// En el schema, marcás qué campos son cacheables:
// type Post @cacheControl(maxAge: 300) { ... }
  1. 3DataLoader cache: ya cubierto — evita queries duplicadas dentro del mismo request.

34. ¿Cuándo usarías `@defer` y `@stream`?

@defer permite que el servidor envíe los datos no críticos en una segunda respuesta incremental, sin bloquear la UI esperando los datos lentos:

graphql
query GetPost($id: ID!) {
  post(id: $id) {
    title
    body
    # estos comentarios tardan — los deferimos
    ... on Post @defer {
      comments {
        body
        author { name }
      }
    }
  }
}

El cliente recibe title y body inmediatamente, y comments cuando el servidor los tiene listos. Requiere Apollo Server 4+ y soporte en el cliente. @stream hace lo mismo para arrays, enviando cada item a medida que se resuelve.


35. ¿Cómo debuggeás una query lenta en producción?

Un proceso estructurado:

  1. 1Apollo Studio / Rover: revisá los traces por operación. Apollo Studio muestra el tiempo de cada resolver individualmente.
  1. 2Logging de resolvers con un plugin:
typescript
const resolverTimingPlugin = (): ApolloServerPlugin => ({
  requestDidStart: async () => ({
    executionDidStart: async () => ({
      willResolveField: ({ info }) => {
        const start = Date.now();
        return () => {
          const duration = Date.now() - start;
          if (duration > 100) {
            console.warn(`Resolver lento: ${info.parentType.name}.${info.fieldName} — ${duration}ms`);
          }
        };
      },
    }),
  }),
});
  1. 3Revisá si hay N+1: si ves muchas queries repetidas en los logs de tu ORM, un DataLoader lo resuelve.
  1. 4Analizá el query plan en Federation: el Router de Apollo puede exportar el execution plan; si hay fetches seriales innecesarios, @provides o reestructurar el schema puede ayudar.

Errores comunes que los entrevistadores buscan

No limpiar el caché del DataLoader entre requests: creás el DataLoader fuera del context factory y todos los requests comparten el caché → datos stale para todos los usuarios.

Exponer stacktraces en producción: sin formatError, Apollo devuelve el stack completo al cliente en errores inesperados.

Schemas circulares sin lazy resolvers: User.posts → Post.author → User.posts puede crear loops. Con lazy resolvers (funciones que devuelven funciones) en makeExecutableSchema de algunos toolkits, o simplemente con resolvers separados, se evita.

Usar type en lugar de input para argumentos de mutations: GraphQL no lo permite y el error de runtime no siempre es claro.

Subscriptions con PubSub in-memory en múltiples instancias: ya mencionado, pero es tan frecuente que vale repetirlo.


Consejos finales para la entrevista

  • Mencioná el N+1 antes de que te lo pregunten. Si hablás de relaciones entre tipos, decí: "y acá usaría DataLoader para evitar el N+1". Demuestra que ya pensaste en producción.
  • Sabé cuándo NO usar GraphQL. Los mejores candidatos no defienden ciegamente una tecnología.
  • Diferenciá errores de negocio de errores de infraestructura. Los primeros pueden ir en el schema como unions; los segundos van en errors.
  • Mencioná APQ y caché en CDN si preguntás por performance. Muchos candidatos olvidan que GraphQL tiene workarounds elegantes para el caché HTTP.
  • Federation es un diferenciador: pocos candidatos la conocen a fondo. Si la nombrás con @key, __resolveReference y @requires, destacás.

Si querés practicar estas respuestas en voz alta antes de la entrevista, [InterviewHack.ai](https://interviewhack.ai) tiene simulaciones de entrevistas técnicas de backend donde podés ensayar exactamente este tipo de preguntas con feedback inmediato.

FAQ

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

REST expone múltiples endpoints y el servidor decide qué datos devuelve. GraphQL usa un único endpoint y el cliente especifica exactamente qué campos necesita, eliminando over-fetching (recibir más datos de los necesarios) y under-fetching (tener que hacer múltiples requests para ensamblar una vista).

¿Qué es el problema N+1 en GraphQL y cómo se soluciona?+

El N+1 ocurre cuando al resolver una lista de N elementos, cada elemento dispara una query adicional a la base de datos, resultando en N+1 queries totales. Se soluciona con DataLoader, que agrupa (batch) múltiples llamadas que ocurren en el mismo tick del event loop y las ejecuta en una sola query.

¿Qué es DataLoader en GraphQL?+

DataLoader es una utilidad que agrupa múltiples peticiones de datos individuales (por ejemplo, cargar el autor de cada post) que ocurren en el mismo tick del event loop, ejecutándolas como una sola query batch. También incluye caché por request para evitar queries duplicadas.

¿Cómo funciona la autenticación en GraphQL con Apollo Server?+

La autenticación se implementa en la función context factory del servidor: se extrae y valida el JWT del header Authorization antes de ejecutar cualquier resolver, y el usuario autenticado se pasa en el objeto context. Los resolvers luego verifican context.currentUser para autorizar operaciones.

¿Qué es Apollo Federation y cuándo la usarías?+

Apollo Federation permite dividir un schema GraphQL grande en múltiples subgraphs mantenidos por equipos distintos. Un Router central los compone en una API unificada. Se usa en arquitecturas de microservicios donde distintos equipos son dueños de distintos dominios de datos.

¿Por qué el caché HTTP no funciona directamente con GraphQL?+

REST usa URLs únicas con métodos GET que CDNs y proxies cachean de forma nativa. GraphQL envía todas las queries al mismo endpoint via POST, lo que impide el caché HTTP estándar. Las soluciones incluyen Automatic Persisted Queries (APQ) para cachear por hash de query, y plugins de response caching a nivel de Apollo Server.

Artículos relacionados

Preguntas de entrevista de Node.js — 50 con código y respuestas

Artículo SEO de 50 preguntas de entrevista de Node.js

Preguntas de entrevista de Redis — 35 con código y respuestas reales

35 preguntas de Redis para entrevistas backend y system design: estructuras de datos, pub/sub, persistencia, clustering, patrones de caché. Con ejemplos de código.

Preguntas de entrevista de Microservicios — 35 respuestas de arquitecto

35 preguntas de microservicios para roles backend y arquitecto: descomposición de servicios, saga pattern, event sourcing, circuit breaker, observabilidad. Respuestas de nivel senior.

System Design: cómo diseñar un sistema de pagos en una entrevista

Guía paso a paso para diseñar un sistema de pagos en una entrevista de system design: idempotencia, consistencia, colas de transacciones, reconciliación, seguridad PCI DSS.

Preparate para tu entrevista real

Pegá el link de tu vacante: investigamos quién te entrevista y te ensayamos en vivo.

Empezar gratis →

¿Tenés entrevista próxima? Instalá el copiloto en vivo →

InterviewHack.ai

Preparate para la entrevista exacta: quién te entrevista, tu CV a medida y coach real.

Producto

VacantesRevisar CV (ATS) gratis¿Cómo suena tu inglés?¿Te pagan bien?Reporte de sueldos LATAMCursos gratisBlogCV a medidaPráctica habladaEs gratis

Empleos remotos

ReactPythonFull-StackLATAMArgentinaMéxicoVer todas →

Preparate

Práctica habladaFrontendBackendAI EngineerPor empresaVendete con tu CV

Empresa

Buscás talentoAcerca deContactoPrivacidadTérminos

© 2026 InterviewHack.ai · Tu CV es tuyo. Nunca se usa para entrenar nada. · Un producto de IA-PTY