GraphQL Interview Questions and How to Answer Them (35+ Questions)
GraphQL interviews separate candidates who have read the docs from those who have built real things with it. This guide covers every question you are likely to face — from "what is GraphQL" to federation, dataloader optimizations, and security hardening — with honest, complete answers and working code.
How to Use This Guide
Work through sections in order if you are new. Jump to the advanced sections if you already ship GraphQL daily. Every answer is written the way a strong senior engineer would say it out loud — not a textbook definition, but the kind of answer that makes an interviewer nod and move on.
Section 1: Fundamentals (Questions 1–10)
1. What is GraphQL and how does it differ from REST?
GraphQL is a query language for APIs and a runtime for executing those queries against your data. Facebook open-sourced it in 2015 after using it internally since 2012.
The core difference from REST is who decides the shape of the response. In REST, the server decides: you hit /users/42 and you get whatever fields the server chose to include. In GraphQL, the client decides: you ask for exactly the fields you need and you get exactly those fields back.
Three structural differences matter most in interviews:
| Dimension | REST | GraphQL |
|---|---|---|
| Endpoint count | Many (/users, /posts, /comments) | Usually one (/graphql) |
| Over/under-fetching | Common | Eliminated by design |
| Versioning | URL versioning (/v2/users) | Schema evolution with @deprecated |
| Type system | Optional (OpenAPI) | Built-in, mandatory |
What interviewers want to hear: You understand the trade-offs, not just the pitch. REST is simpler to cache at the HTTP layer, easier to reason about for simple CRUD, and has broader tooling. GraphQL shines when clients have divergent data needs (mobile vs. web), when you are aggregating multiple services, or when you want to iterate on the API without breaking consumers.
2. Explain the three root operation types in GraphQL.
Every GraphQL operation belongs to one of three types:
Query — read-only data fetching. The safe, idempotent operation.
query GetUser($id: ID!) {
user(id: $id) {
name
email
posts {
title
}
}
}Mutation — writes that change server state. Semantically equivalent to POST/PUT/PATCH/DELETE in REST.
mutation CreatePost($input: CreatePostInput!) {
createPost(input: $input) {
id
title
createdAt
}
}Subscription — long-lived connections for real-time data. The server pushes updates to the client when data changes. Typically implemented over WebSockets.
subscription OnNewMessage($roomId: ID!) {
messageAdded(roomId: $roomId) {
id
body
author {
name
}
}
}Important nuance: Queries can run in parallel because they are side-effect-free. Mutations in a single request run sequentially by the spec — the first mutation completes before the second starts.
3. What is a GraphQL schema and what is SDL?
The schema is the contract between client and server. It defines every type, every field on every type, and every operation the API supports. Nothing can be queried that is not in the schema.
SDL (Schema Definition Language) is the human-readable syntax for writing schemas:
type User {
id: ID!
name: String!
email: String!
role: UserRole!
posts: [Post!]!
createdAt: DateTime!
}
enum UserRole {
ADMIN
EDITOR
VIEWER
}
type Post {
id: ID!
title: String!
body: String!
author: User!
tags: [String!]!
publishedAt: DateTime
}
type Query {
user(id: ID!): User
users(limit: Int = 10, offset: Int = 0): [User!]!
post(id: ID!): Post
}
type Mutation {
createPost(input: CreatePostInput!): Post!
deletePost(id: ID!): Boolean!
}
input CreatePostInput {
title: String!
body: String!
tags: [String!]
}The ! means non-nullable. [Post!]! means the list itself is non-null and each item in the list is non-null.
4. What are resolvers and how do they work?
Resolvers are the functions that fulfill each field in the schema. When a query arrives, the GraphQL runtime walks the query tree and calls the matching resolver for each field.
Every resolver receives four arguments:
parent(orroot) — the resolved value of the parent fieldargs— the arguments passed to this field in the querycontext— shared data for the whole request (auth user, database connection, etc.)info— metadata about the query execution (field name, return type, etc.)
const resolvers = {
Query: {
user: async (parent, { id }, context, info) => {
return context.db.users.findById(id);
},
users: async (parent, { limit, offset }, context) => {
return context.db.users.findAll({ limit, offset });
},
},
User: {
posts: async (parent, args, context) => {
// parent is the User object resolved above
return context.db.posts.findByAuthorId(parent.id);
},
},
Mutation: {
createPost: async (parent, { input }, context) => {
if (!context.user) throw new Error('Unauthenticated');
return context.db.posts.create({ ...input, authorId: context.user.id });
},
},
};Default resolver behavior: If you do not define a resolver for a field, GraphQL uses a default resolver that simply returns parent[fieldName]. So for scalar fields on an object (like user.name), you often do not need to write a resolver at all.
5. What is the N+1 problem in GraphQL and how do you solve it?
This is one of the most common interview questions. Get it right.
The problem: Imagine you fetch a list of 10 posts and each post has an author field. Without optimization:
- 1One query fetches 10 posts → 1 database query
- 2For each post, the
authorresolver fires individually → 10 more queries - 3Total: 11 queries when 2 would suffice
The solution: DataLoader
DataLoader batches and caches requests that happen within the same tick of the event loop.
import DataLoader from 'dataloader';
function createUserLoader(db) {
return new DataLoader(async (userIds) => {
const users = await db.users.findByIds(userIds);
return userIds.map(id => users.find(u => u.id === id));
});
}
const context = {
db,
loaders: {
user: createUserLoader(db),
},
};
const resolvers = {
Post: {
author: (post, args, context) => {
return context.loaders.user.load(post.authorId);
},
},
};Now 10 posts load their authors in a single SQL WHERE id IN (1,2,...,10) query.
Key points: Create DataLoader instances per request (not per server) to avoid leaking data between users. DataLoader also caches within the request. The batch function must return values in the same order as the input keys.
6. What are GraphQL variables and why should you use them?
Variables let you pass dynamic values into a query without string interpolation. They make queries reusable and prevent injection attacks.
query GetUser($id: ID!) {
user(id: $id) {
name
email
}
}Variables are sent separately as JSON: { "id": "42" }. Variable type modifiers: String! (required), Int = 10 (optional with default), [String!] (optional array).
7. What are fragments in GraphQL?
Fragments are reusable units of fields. They prevent repeating the same field selections across multiple queries.
fragment UserBasic on User {
id
name
email
avatarUrl
}Inline fragments (... on TypeName) are used for interface and union types to access type-specific fields in a polymorphic result.
8. What is the difference between a type and an input type?
Types (type User) are used in output — they describe what the server returns. Input types (input CreateUserInput) are used in arguments — they describe what the client sends. You cannot use a regular type as a mutation argument. Inputs are plain data containers without resolvers.
9. What are directives in GraphQL?
Directives are annotations with the @ prefix that modify execution behavior.
Built-in directives: @include(if: Boolean) — include field only if true. @skip(if: Boolean) — skip field if true. @deprecated(reason: String) — marks a schema field as deprecated.
Custom schema directives are powerful for cross-cutting concerns like auth, rate limiting, and caching without polluting resolver logic.
10. How does GraphQL handle errors?
GraphQL has a distinctive error model: a response can contain both data and errors at the same time.
{
"data": { "user": { "name": "Ana Gomez", "posts": null } },
"errors": [{ "message": "Database timeout", "path": ["user", "posts"] }]
}Three error patterns: (1) null the field and add to errors array (default), (2) throw in a non-nullable field to bubble up to the nearest nullable ancestor, (3) union error types — model expected failures in the schema so clients know what can fail at compile time.
Section 2: Schema Design (Questions 11–18)
11. How do you design a good GraphQL schema?
Design for the client, not the database. Use strong domain-specific types — avoid JSON scalar for things that have structure. ! is a promise to the client; be careful. Use Relay connections for paginated lists. Group mutations by domain namespace to avoid a flat list of 50 mutations.
12. What are interfaces and union types? When do you use each?
Interface — defines fields implementing types must include. Use when multiple types share fields meaningful to query together (id, createdAt).
Union — a field can return one of several unrelated types. Use when types share no common fields (search results returning Users, Posts, and Products).
Querying interfaces lets you read shared fields directly; querying unions requires inline fragments for every field.
13. How does pagination work in GraphQL?
Three patterns: offset (simple, but unstable on inserts/deletes), cursor-based (recommended — opaque string, stable pages regardless of mutations), page-number (fine for admin UIs). Cursor-based pagination with the Relay connection spec (PostConnection, PostEdge, PageInfo) is the production standard.
14. How do you handle authentication and authorization in GraphQL?
Authentication happens before resolvers in the context factory — verify JWT, attach user to context. Authorization patterns: check in resolver (simple, repetitive), schema directives (declarative), or a separate permission layer like graphql-shield (most scalable for large schemas).
15. What is schema stitching vs. schema federation?
Schema Stitching: code-level composition where the gateway merges schemas programmatically. You own the merge logic. More flexible. Apollo Federation: protocol-level composition where subgraphs declare ownership via @key directives. The gateway (Apollo Router) is thin and auto-generates query plans. Use Federation for greenfield microservices with team autonomy; stitching for existing services you cannot modify.
16. What are custom scalars and when should you use them?
Built-in scalars: String, Int, Float, Boolean, ID. Custom scalars implement serialize (output), parseValue (variable input), and parseLiteral (inline literal). Use graphql-scalars for production-ready DateTime, EmailAddress, URL, UUID, JSON.
17. How do you implement soft delete vs. hard delete in a GraphQL API?
Soft delete: add deletedAt: DateTime to the type. Resolvers filter WHERE deletedAt IS NULL by default. Expose deletePost (sets timestamp) and restorePost (clears it). Both mutations return the affected object rather than a boolean for better client feedback.
18. How do you version a GraphQL API?
Do not version — evolve. Adding fields is always safe. Deprecate with @deprecated(reason: "...") and keep old fields alive during client migration. Document the sunset date in the deprecation reason. Only introduce a separate schema version for wholesale rewrites.
Section 3: Performance and Production (Questions 19–28)
19. How do you handle caching in GraphQL?
Four layers: in-resolver LRU cache for hot data, Apollo response cache plugin with @cacheControl schema directives, persisted queries for CDN GET-request cacheability, and DataLoader for per-request deduplication. Each layer addresses a different scope and granularity.
20. What is query complexity analysis and why does it matter?
Without limits, users(100) { friends(100) { friends(100) { name } } } produces 10^6 database calls. Query complexity assigns a cost per field and rejects queries exceeding a threshold. graphql-query-complexity lets you set per-field costs. graphql-depth-limit is simpler (max nesting levels) but less precise.
21. How do you implement rate limiting in GraphQL?
HTTP-level rate limiting is too blunt — one request can be cheap or catastrophically expensive. Better: track complexity budget per user per minute in Redis (INCRBY key complexity), reject when the user exceeds the budget. This ties rate limits to actual server cost.
22. What are subscriptions and how are they implemented?
Subscriptions use WebSockets (graphql-ws). The server publishes events via PubSub when mutations change data; subscribers receive updates. The in-memory PubSub is single-process only — use graphql-redis-subscriptions in production with horizontal scaling.
23. How do you secure a GraphQL API in production?
Disable introspection, add depth and complexity limits, disable unnecessary query batching, use persisted queries as a whitelist, field-level authorization, custom scalar validation, rate limiting, HTTP security headers (helmet), and strip internal error details in formatError before sending to clients.
24. How do you monitor and observe a GraphQL API?
Key metrics: operation name distribution, field usage (tracks safe deprecation), error rate per operation, resolver execution time, cache hit rate. Apollo Studio provides this out of the box. Custom plugins can instrument slow resolvers and emit histograms to any observability backend.
25. What is the difference between lazy loading and eager loading in GraphQL resolvers?
Lazy (default): each resolver fetches independently — simple but N+1 risk. Eager: parent loads related data via SQL JOIN — but wastes resources if the client did not request those fields. Correct solution: DataLoader — lazy semantics (only fetches what is requested) with eager performance (batches into one query).
26. How do you handle file uploads in GraphQL?
Use the graphql-multipart-request-spec with the Upload scalar and graphql-upload Express middleware. Validate mimetype in the resolver, then stream to S3 (or another store). In Apollo Server 3+, file upload support was removed from core and must be added explicitly.
27. What is deferred execution and the `@defer` directive?
@defer lets clients request that a fragment be streamed after the initial response via multipart HTTP. The server sends fast fields first, then deferred fragments arrive as they resolve. Use it when part of a query is expensive (AI-generated content, slow external APIs) and you want to unblock the initial render.
28. How do you implement real-time search with GraphQL?
Two approaches: debounced useLazyQuery fires a query after a delay on each keystroke — simple, no subscriptions needed. searchResults subscription pushes live results when server-side data changes — needed when results must update for all users simultaneously, not just the typing user.
Section 4: Advanced Topics (Questions 29–42)
29. How does Apollo Federation work at a technical level?
Each subgraph implements the Federation spec: @key(fields: "id") declares the entity primary key, __resolveReference resolves the entity from that key. The router builds a query plan tree, executes fetches to relevant subgraphs (in parallel where possible), and merges results into one response. Subgraphs extend each other's types via extend type User @key(fields: "id") + @external.
30. What is the difference between schema-first and code-first approaches?
Schema-first: SDL is the source of truth — write schema, implement resolvers. Good for team collaboration; needs graphql-codegen for full type safety. Code-first (TypeGraphQL, Nexus): define types programmatically, SDL is generated from code — native TypeScript type safety without code generation, but schema-as-documentation is less natural.
31. How does `graphql-codegen` work and why is it important?
It reads your schema and .graphql operation files, then generates TypeScript types, input types, and React hooks. Your components get full type safety for query variables and response shapes without writing types manually. Run it in watch mode during development and as part of CI.
32. What are GraphQL persisted queries and automatic persisted queries (APQ)?
Persisted queries: pre-register queries at build time, clients send only the hash — smaller payloads, CDN GET cacheability, and a whitelist security model. APQ: a negotiation protocol — client sends hash first; on cache miss, server requests full query, stores it, and responds. Future requests use the hash only.
33. How do you test GraphQL resolvers?
Unit: call resolver functions directly with a mocked context — fast and isolated. Integration: use server.executeOperation() with a real schema and test database — exercises the full resolver tree without HTTP overhead. Always test error paths: unauthenticated access, not-found inputs, and validation failures.
34. How does the GraphQL execution algorithm work internally?
Parse (query string → AST), Validate (AST against schema — rejects invalid queries before any resolver runs), Execute (walk the AST, call resolvers, await Promises), Coerce (serialize scalars for the response). Queries resolve fields in parallel; top-level mutation fields run sequentially per spec.
35. What is the `context` object and what should go in it?
Context is a per-request dependency injection container. Put in: authenticated user, DB connection, DataLoader instances (per-request), external service clients, request-scoped logger, feature flags. Do not put in: state only one resolver needs, mutable state you modify mid-execution.
36. What are the key differences between Apollo Client and urql?
Apollo Client: larger ecosystem, normalized InMemoryCache, Apollo Studio integration, ~32 KB bundle. urql: composable exchange pipeline, optional normalized cache, ~7 KB bundle, simpler defaults. Pick Apollo for enterprise apps or Federation; pick urql for smaller projects where bundle size matters or you want full control.
37. How does the InMemoryCache work in Apollo Client?
It normalizes data into a flat store keyed by TypeName:id. Updating User:42 in a mutation automatically propagates to every query that reads User:42. Use cache.modify() for manual updates when adding or removing items from a list — Apollo cannot infer list changes automatically.
38. What is optimistic UI and how do you implement it in Apollo Client?
Provide an optimisticResponse in useMutation. Apollo writes it to cache immediately; the UI updates before the server responds. When the real response arrives, it replaces the optimistic data. If the server errors, Apollo reverts to the pre-mutation state automatically.
39. Explain the `@defer` and `@stream` directives.
@defer delays an entire fragment. @stream delivers list items incrementally as they resolve. Both use multipart HTTP or SSE. Supported by Apollo Router, GraphQL Yoga, and Mercurius. Use @stream for long lists where early items should render before the full list is ready.
40. How do you implement a connection between two existing services using GraphQL federation?
User service: type User @key(fields: "id") { id name email }. Order service: extend type User @key(fields: "id") { id: ID! @external; orders: [Order!]! } plus __resolveReference({ id }) { return { id } }. The router's query plan automatically fetches user fields from user-service and order fields from order-service, then merges the response.
41. What are some common GraphQL anti-patterns to avoid?
Mirroring the database schema, overusing JSON scalar, marking every field non-null, giant flat mutation lists, resolvers doing business logic (put that in a service layer), missing DataLoader on any list relationship, leaking internal errors to clients, removing deprecated fields without a migration window.
42. How would you migrate a REST API to GraphQL incrementally?
Phase 1: GraphQL facade over REST — resolvers call existing REST endpoints, zero backend changes, validate the approach. Phase 2: migrate high-value resolvers to hit the DB directly. Phase 3: new services expose GraphQL natively, facade thins out. Phase 4: decommission REST routes once no consumers remain. Never force a hard cutover.
Section 5: Interview Meta-Questions (Questions 43–45)
43. When should you NOT use GraphQL?
Simple CRUD with one client, APIs requiring aggressive HTTP caching without APQ setup, file-heavy transfer scenarios better served by presigned S3 URLs, simple public APIs consumed by many external parties (REST is more universally understood), and small teams moving fast where the schema design overhead outweighs the benefits.
44. What is your process for debugging a slow GraphQL query?
Enable tracing to find slow resolvers. Check for N+1. Inspect generated SQL and missing indexes. Check whether resolvers load data the client did not request. Profile for external API latency vs. DB latency. Consider resolver-level TTL caching. Consider field-level pagination if returning huge lists.
45. How would you explain GraphQL to a non-technical stakeholder?
"Think of REST like a fixed combo meal — you get everything on the plate whether you want it or not. GraphQL is ordering a la carte: you tell the kitchen exactly what you want and that is exactly what arrives. Our mobile app gets a lightweight response; our web app gets full detail — from the same endpoint, without maintaining two separate APIs."
Quick Reference Cheat Sheet
Core concepts: Schema = contract, Resolver = field fetcher, Context = per-request DI container, DataLoader = batch + cache per request.
Performance checklist: DataLoader for all relationship fields, query complexity + depth limits, disable introspection in production, response caching for public queries, persisted queries for CDN cacheability.
Error handling: Union types for expected errors, thrown errors for unexpected ones, never expose internals in production.
Schema design: Design for clients not databases, non-null is a promise so use carefully, use connections for paginated lists, deprecate before removing fields.
Testing: Unit test resolvers with mocked context, integration test with executeOperation (no HTTP layer needed), always test error paths.
Closing Thoughts
The engineers who do best in GraphQL interviews are the ones who have genuinely built something with it and hit the hard edges — the N+1 problem on a real database, a subscription that leaked memory because the DataLoader was created at server start, a union type that needed a __resolveType function. Study this guide, then build something. The questions above will feel familiar when you have debugged them in production.