GraphQL interviews are rarely about memorizing syntax. Strong answers explain the execution model, identify operational risks, and show when a different API style would be simpler.
This guide contains 29 questions for frontend and backend interviews. It covers schemas, resolvers, DataLoader, authentication, errors, demand control, cursor pagination, subscriptions, and Apollo Client 4.
Table of Contents
- GraphQL Fundamentals Questions
- GraphQL vs REST Questions
- Schema Design Questions
- Resolver Questions
- N+1 and DataLoader Questions
- Authentication and Authorization Questions
- Error Handling Questions
- Performance and Security Questions
- Pagination Questions
- Subscriptions Questions
- Apollo Client Questions
- GraphQL Scenario Questions
GraphQL Fundamentals Questions
1. What is GraphQL and what problem does it solve?
GraphQL is a query language and execution model for APIs. A typed schema defines the fields clients may request, and the server validates and executes an operation against that schema.
Client-selected fields can reduce unnecessary response data and let one operation traverse related objects. They do not guarantee one database call, remove server-side over-fetching, or eliminate the need for careful API and data-access design.
The core pieces are:
- schema: the API contract;
- operations: queries, mutations, and subscriptions;
- resolvers: functions that supply field values;
- validation and execution: the rules that turn an operation into a response.
GraphQL itself does not require HTTP, a single endpoint, a particular database, or a specific server framework.
2. How would you explain GraphQL in 30 seconds?
“GraphQL lets an API publish a typed graph of fields and lets a client select the fields it needs. The server validates the operation against the schema and resolves those fields from databases, services, or other sources. It is useful when clients need different or nested data shapes, but it adds schema governance, demand-control, caching, and observability work.”
That answer is more defensible than claiming that GraphQL always replaces multiple REST calls or removes all over-fetching.
3. What are GraphQL's main benefits and trade-offs?
Potential benefits:
- a typed contract with validation, introspection, and strong tooling;
- client-selected response shapes;
- a graph that can compose data from multiple sources;
- additive schema evolution and explicit deprecation;
- operation-level metrics and generated client types.
Trade-offs:
- field-level authorization and error behavior require discipline;
- naive resolvers can cause N+1 calls or duplicate work;
- public clients can submit costly operations unless demand is controlled;
- normalized client caching and pagination need policies;
- schema ownership, compatibility, and observability become organizational concerns.
GraphQL is a design choice, not an automatic upgrade over REST, RPC, or event-driven interfaces.
GraphQL vs REST Questions
4. How does GraphQL differ from REST?
REST is an architectural style built around resources, representations, uniform interfaces, and HTTP semantics. GraphQL defines a typed schema and an operation language in which the client selects fields.
A GraphQL-over-HTTP API commonly exposes one URL, but that is a convention rather than a rule in the core specification. A REST API can also support sparse fields, includes, compound documents, and efficient caching. Compare concrete contracts and workloads rather than caricatures:
| Concern | GraphQL | REST-style HTTP API |
|---|---|---|
| Response shape | selected in an operation | defined by each resource representation |
| Contract | executable type system | HTTP semantics plus OpenAPI or another contract |
| Caching | often client or gateway aware of operations | can use standard HTTP cache keys and validators |
| Errors | request and field errors; partial data is possible | status code plus application error representation |
| Evolution | additive fields and deprecation | versioning or compatible representation changes |
5. When should you choose GraphQL over REST?
GraphQL is a good candidate when several clients need different views of connected data, a schema can unify multiple services, and the organization can operate schema governance and demand controls.
REST may be simpler for stable resource operations, downloads, CDN-heavy public content, straightforward CRUD, or a team without a reason to absorb GraphQL's operational cost. File transfer can coexist with GraphQL through signed URLs or a separate HTTP endpoint.
Ask these questions before choosing:
- Do clients genuinely need flexible or nested selections?
- Who owns schema compatibility and deprecation?
- Can the platform measure and limit operation cost?
- How will authorization remain consistent across entry points?
- Does the client ecosystem benefit from GraphQL tooling?
Schema Design Questions
6. What is the GraphQL Schema Definition Language?
SDL is a human-readable notation for a GraphQL schema. It describes object types, fields, arguments, interfaces, unions, enums, scalars, input objects, and directives.
Query is required as an operation root. Mutation and Subscription are optional.
scalar DateTime
type Query {
post(id: ID!): Post
posts(first: Int!, after: String): PostConnection!
}
type Mutation {
createPost(input: CreatePostInput!): CreatePostPayload!
}
type Post {
id: ID!
title: String!
publishedAt: DateTime
}
input CreatePostInput {
title: String!
}
type CreatePostPayload {
post: Post
userErrors: [UserError!]!
}Introspection makes the schema discoverable to tools, but useful documentation still requires names, descriptions, examples, and lifecycle guidance.
7. How do input and output types differ?
Input positions accept scalars, enums, and input objects, wrapped in list or non-null modifiers. Output positions accept scalars, enums, objects, interfaces, and unions. An output object such as User cannot be used as a mutation input, and an input object cannot be returned as a field type.
input UpdateProfileInput {
displayName: String
interests: [String!]
}
type User {
id: ID!
displayName: String!
interests: [String!]!
}
union SearchResult = User | PostSeparate types allow the write contract to differ from server-generated and relationship-rich output data.
8. What do [Post]!, [Post!], and [Post!]! mean?
The exclamation mark makes the type immediately to its left non-null:
type Feed {
a: [Post]! # list non-null; individual items may be null
b: [Post!] # list may be null; items may not be null
c: [Post!]! # neither list nor items may be null
}If a selected nullable field cannot produce a value, it can resolve to null. An error on a non-null field propagates to the nearest nullable parent and may null a larger part of the response. Choose nullability from the domain guarantee, not from a blanket rule. Tightening nullability later can be a breaking change.
Resolver Questions
9. What are resolvers, and which arguments do they receive?
A resolver supplies the value for a field. In GraphQL.js and many JavaScript frameworks, its signature is (source, args, context, info):
const resolvers = {
Query: {
post: (_root, { id }, context, info) =>
context.services.posts.getById({ id, actor: context.actor })
},
Post: {
author: (post, _args, context) =>
context.loaders.userById.load(post.authorId)
}
};sourceis the value returned for the parent object; for a root field it is the configured root value;argscontains coerced field arguments;contextcarries request-scoped identity, services, and loaders;infodescribes the field, schema, operation, and execution path.
This four-argument API is an implementation convention, not part of the language syntax for every GraphQL runtime.
10. Why keep resolvers thin?
Thin resolvers adapt the GraphQL transport to trusted application services. Business rules and authorization then have one source of truth that can also serve REST endpoints, jobs, or message consumers.
const resolvers = {
Mutation: {
createPost: (_root, { input }, context) =>
context.services.posts.create({ input, actor: context.actor })
}
};Resolvers can still own transport concerns such as translating arguments, mapping domain outcomes to GraphQL types, and attaching field-specific metadata. “Thin” does not mean hiding every GraphQL decision in a generic service.
N+1 and DataLoader Questions
11. What is the N+1 problem in GraphQL?
N+1 occurs when one call loads a list of N parents and a nested resolver makes another call for each parent. For 100 users, a naive posts resolver may issue one user query plus 100 post queries.
const resolvers = {
Query: { users: () => db.user.findMany() },
User: {
posts: user => db.post.findMany({ where: { authorId: user.id } })
}
};GraphQL does not inherently cause N+1. It exposes the problem when independently written field resolvers use an unsuitable fetching strategy. Joins, prefetching, ORM relation loaders, query planning, gateway batching, or DataLoader may be appropriate depending on the source.
12. How does DataLoader address N+1?
DataLoader coalesces .load(key) calls into batches and memoizes loads within a configured cache scope. Its batch function must return a promise for an array with the same length and order as the input keys; each position contains a value or an Error.
import DataLoader from "dataloader";
function createUserLoader(db) {
return new DataLoader(async ids => {
const users = await db.user.findMany({ where: { id: { in: [...ids] } } });
const byId = new Map(users.map(user => [user.id, user]));
return ids.map(id => byId.get(id) ?? new Error(`User ${id} not found`));
}, { maxBatchSize: 500 });
}The JavaScript library uses a batch scheduling frame, which can be customized; “one event-loop tick” is an approximation, not its contract. Batch size should respect database parameter limits and downstream quotas.
13. Why are DataLoader instances usually request-scoped?
DataLoader's cache is memoization, not a shared application cache. A request-scoped instance avoids leaking values between users or tenants and prevents stale values from living indefinitely.
async function buildContext({ request }) {
const actor = await authenticateRequest(request);
return {
actor,
loaders: { userById: createUserLoader(db.forTenant(actor.tenantId)) }
};
}Longer-lived loaders can be valid only when keys include every authorization and tenancy dimension and the cache has an explicit invalidation policy. After a mutation, clear or prime affected request-local entries so later fields do not observe stale data.
Authentication and Authorization Questions
14. How should authentication work in GraphQL?
Authenticate before field execution, usually in transport middleware or the context factory. Parse the credential strictly and validate its signature or session, issuer, audience, algorithm, time constraints, and revocation policy as applicable. Pass a verified principal—not an untrusted token payload—to the rest of the application.
async function buildContext({ request }) {
const actor = await auth.authenticate(request.headers.get("authorization"));
return {
actor,
services: createServices({ actor }),
loaders: createLoaders({ actor })
};
}Decide explicitly whether missing credentials create an anonymous context and whether invalid credentials reject the request. Do not silently treat a malformed token as anonymous if that changes security behavior.
15. What is the difference between authentication and authorization?
Authentication establishes identity. Authorization decides whether that identity may perform an action or see data.
The GraphQL context is a convenient place to carry the verified principal. Authorization rules should live in a trusted business or domain layer so the same policy protects GraphQL fields, REST endpoints, background jobs, and other entry points.
const resolvers = {
Mutation: {
deletePost: (_root, { id }, context) =>
context.services.posts.delete({ id, actor: context.actor })
}
};Field resolvers may invoke authorization, but they should not become the only copy of the policy.
16. How does directive-based authorization work?
A custom directive can annotate policy intent in SDL:
directive @auth(requires: Role!) on FIELD_DEFINITION | OBJECT
enum Role { USER ADMIN }
type Query {
viewer: User! @auth(requires: USER)
auditLog: [AuditEvent!]! @auth(requires: ADMIN)
}The annotation does nothing by itself. Server code must interpret it through schema transformation, instrumentation, or resolver wrapping and delegate the actual decision to the policy layer. Test object fields, interfaces, fragments, default behavior, and newly added fields; a default-deny posture is safer for sensitive types.
Error Handling Questions
17. How does GraphQL error handling work?
GraphQL distinguishes errors that prevent execution from field errors raised during execution. A field error can produce partial data, depending on the field's nullability; an error on a non-null field propagates to a nullable parent.
{
"data": { "user": { "name": "Alice", "posts": null } },
"errors": [
{
"message": "Posts are temporarily unavailable",
"path": ["user", "posts"],
"extensions": { "code": "DEPENDENCY_UNAVAILABLE" }
}
]
}Errors can include message, locations, path, and extensions. Mask stack traces, SQL messages, secrets, and personal data. The GraphQL-over-HTTP response status depends on whether a valid GraphQL response was produced; clients should not assume every GraphQL error means HTTP 500.
18. When should errors be modeled as union types?
A result union can model expected domain outcomes that clients are meant to branch on:
union CreateUserResult = User | EmailTaken | InvalidInput
type EmailTaken {
message: String!
}
type InvalidInput {
message: String!
field: String
}
type Mutation {
createUser(input: CreateUserInput!): CreateUserResult!
}This is a design pattern, not a universal rule. It gives typed outcomes but requires fragments and can enlarge a schema. Unexpected infrastructure failures should still be reported and masked as execution errors rather than represented as normal business success paths.
Performance and Security Questions
19. How do you protect a GraphQL API from expensive operations?
Use layered demand control rather than one magic depth or cost number:
- require pagination and cap list sizes;
- limit depth, nested-list depth, breadth, aliases, and batched operations;
- estimate schema-aware cost using list arguments and expensive fields;
- apply identity- and cost-aware rate limits;
- set timeouts, cancellation, concurrency limits, and backpressure;
- cap DataLoader batches and downstream work;
- validate and sanitize input values;
- use trusted documents where clients are controlled;
- monitor operation names, fingerprints, latency, errors, and actual resource cost.
Depth alone misses shallow operations with hundreds of aliases. Cost thresholds must be measured against the application's schema, data distribution, and service budgets.
Disabling introspection may reduce discoverability in some first-party deployments, but it is not authorization. Restricting an interactive IDE is also separate from enabling or disabling introspection.
20. How do persisted operations differ from trusted documents?
“Persisted query” is used for two related mechanisms:
- automatic persisted queries (APQ) let a client send a hash and, when unknown, often retry with the full operation so the server can cache it;
- trusted documents are pre-registered, reviewed operations, and production rejects unknown document IDs.
APQ primarily reduces payload size and can avoid repeated parsing. It is not automatically an allowlist. Trusted documents reduce the arbitrary-operation surface for controlled clients, but approved operations can still be abused with costly variables, repetition, or stolen credentials. Keep authorization, variable bounds, rate limits, and demand controls.
Pagination Questions
21. What is cursor-based pagination?
Cursor pagination continues from an opaque position in a stable total ordering. A robust cursor commonly encodes the sort value plus a unique tie-breaker and may include scope or version information. It should be signed or encrypted when its contents must not be forgeable or visible.
type Query {
posts(first: Int!, after: String): PostConnection!
}
type PostConnection {
edges: [PostEdge!]!
pageInfo: PageInfo!
}
type PostEdge {
node: Post!
cursor: String!
}
type PageInfo {
hasNextPage: Boolean!
endCursor: String
}Connection-style pagination is a widely used convention, especially in Relay-compatible APIs, but GraphQL does not require it. Offset pagination can still be appropriate for small or mostly static datasets.
22. How do you implement keyset cursor pagination?
Define one stable ordering, encode all ordering components, validate first, and fetch one extra row. This simplified forward-only example orders by createdAt DESC, id DESC:
const encodeCursor = post => Buffer.from(JSON.stringify({
createdAt: post.createdAt.toISOString(),
id: post.id
})).toString("base64url");
const decodeCursor = value => JSON.parse(
Buffer.from(value, "base64url").toString("utf8")
);
async function posts(_root, { first, after }, context) {
if (!Number.isInteger(first) || first < 1 || first > 100) {
throw new Error("first must be between 1 and 100");
}
const cursor = after ? decodeCursor(after) : null;
const olderThanCursor = cursor ? {
OR: [
{ createdAt: { lt: new Date(cursor.createdAt) } },
{ createdAt: new Date(cursor.createdAt), id: { lt: cursor.id } }
]
} : undefined;
const rows = await context.db.post.findMany({
where: { tenantId: context.actor.tenantId, ...olderThanCursor },
orderBy: [{ createdAt: "desc" }, { id: "desc" }],
take: first + 1
});
const page = rows.slice(0, first);
return {
edges: page.map(node => ({ node, cursor: encodeCursor(node) })),
pageInfo: {
hasNextPage: rows.length > first,
endCursor: page.length ? encodeCursor(page.at(-1)) : null
}
};
}Production code must reject malformed cursors and define behavior for updates, deletions, filter changes, and snapshot consistency. totalCount is optional because it may be costly or inconsistent with a changing dataset.
Subscriptions Questions
23. What are GraphQL subscriptions, and when should you use them?
A subscription is a long-lived operation that creates an event stream and executes a response operation for each event. The GraphQL specification describes subscription execution but does not mandate a transport. WebSockets and server-sent events are common choices.
Use subscriptions when clients need frequent incremental updates close to real time, such as active collaboration or live telemetry. For infrequent changes, polling, refetch-on-interaction, mobile push, or webhooks can be simpler and cheaper.
Plan for reconnects, resubscription, races between an initial query and the stream, backpressure, delivery semantics, and changing authorization during a long-lived connection.
24. How do you implement subscriptions with pub/sub?
A subscription resolver commonly consumes events from a broker while a committed write publishes an event. An in-memory emitter is useful for a demo but cannot coordinate multiple processes and provides little durability or backpressure.
const resolvers = {
Mutation: {
createPost: (_root, { input }, context) =>
context.services.posts.createAndEnqueueEvent({ input, actor: context.actor })
},
Subscription: {
postAdded: {
subscribe: (_root, _args, context) => {
context.policies.requireFeedAccess(context.actor);
return context.events.subscribe({
topic: "POST_ADDED",
tenantId: context.actor.tenantId
});
},
resolve: (event, _args, context) => {
context.policies.requirePostAccess(context.actor, event.post);
return event.post;
}
}
}
};Use an outbox or another atomic publication design so a database commit and event publication cannot diverge. Isolate topics by tenant, authorize both subscription creation and delivered events, and choose a broker from required ordering, durability, replay, throughput, and failure semantics—not by name alone.
Apollo Client Questions
25. How does Apollo Client's normalized cache work?
InMemoryCache tries to identify objects using __typename plus key fields, commonly id, then stores references to normalized entities. A later response for the same cache ID can update all queries that reference that entity.
import { ApolloClient, InMemoryCache } from "@apollo/client";
const client = new ApolloClient({
uri: "/graphql",
cache: new InMemoryCache({
typePolicies: {
Product: { keyFields: ["sku"] },
Query: {
fields: {
feed: {
keyArgs: ["filter"],
merge(existing = { edges: [] }, incoming) {
return { ...incoming, edges: [...existing.edges, ...incoming.edges] };
}
}
}
}
}
})
});Normalization does not remove all cache maintenance. Creating or deleting list members can require cache.modify, eviction, a field policy, or selective refetching. Pagination needs deduplication, optimistic writes need rollback behavior, and logout or tenant changes may require clearing sensitive cached data.
26. How do you use useQuery in Apollo Client 4?
In Apollo Client 4, import React hooks from @apollo/client/react and core values such as gql from @apollo/client:
import { gql } from "@apollo/client";
import { useQuery } from "@apollo/client/react";
const GET_USER = gql`
query GetUser($id: ID!) {
user(id: $id) { id name }
}
`;
function UserProfile({ userId }) {
const { data, error, loading, networkStatus } = useQuery(GET_USER, {
variables: { id: userId },
notifyOnNetworkStatusChange: true
});
if (loading && !data) return <Spinner />;
if (error && !data) return <ErrorState />;
return <Profile user={data?.user} refreshing={networkStatus !== 7} />;
}Real components must define fetchPolicy, errorPolicy, partial-data behavior, empty states, and variable changes deliberately. Do not display raw server error messages if they can contain sensitive details.
GraphQL Scenario Questions
27. How would you design a GraphQL API for a social feed?
Start with access patterns and policy, not a list of nouns. Define a cursor-paginated feed with a stable ranking order, bounded page size, viewer-aware visibility, and explicit nullability. Batch author and engagement lookups only where measurements show repeated calls.
Separate public profile fields from sensitive fields, enforce authorization in the domain layer, and include tenant or viewer scope in loader and cache keys. For live updates, decide whether subscriptions are worth their delivery and reconnection cost; a notification followed by a refetch may be safer than streaming fully materialized posts. Measure fan-out, cache hit rate, data-source calls, and per-operation cost.
28. How would you migrate from REST to GraphQL?
Use an incremental strangler approach:
- identify client journeys and design a schema around domain behavior;
- resolve fields through existing services or REST contracts where that preserves business rules;
- add contract checks, authorization tests, operation metrics, and demand controls;
- migrate selected clients and compare correctness, latency, load, and error semantics;
- move internals only where evidence supports it;
- deprecate old contracts after usage reaches an agreed threshold.
Do not assume resolvers should access the database directly. Preserve transactions, authorization, caching, idempotency, status/error semantics, and ownership that the existing service already provides.
29. How would you debug a slow GraphQL API?
Begin with evidence. Identify the operation fingerprint, variables, affected users, latency percentile, and time window. Trace field execution and downstream database, HTTP, and broker spans.
Then test hypotheses: N+1 calls, slow query plans, missing indexes, oversized lists, poor batching, low cache hit rate, serial service calls, resolver contention, or downstream saturation. Inspect actual DataLoader batch sizes and error rates rather than adding it reflexively. Reproduce with production-like cardinality, fix the measured bottleneck, and add a regression benchmark or alert. Demand controls contain damage; they do not replace root-cause analysis.
Quick Reference
| Concept | Interview-safe summary |
|---|---|
| Query | Read-oriented operation; root fields may execute in parallel |
| Mutation | Write-oriented operation; root fields in one operation execute serially |
| Subscription | Long-lived event-stream operation; transport is implementation-specific |
| Resolver | Supplies a field value from a source, service, or calculation |
| Schema | Executable type-system contract |
| DataLoader | Batches and memoizes compatible loads within a chosen scope |
| Fragment | Reusable field selection |
| Directive | Schema or operation annotation whose behavior must be implemented |
Frequently Asked Questions
What is GraphQL and how does it differ from REST?
GraphQL is a query language and execution model for APIs in which a typed schema defines available fields and clients select the fields they need. REST is an architectural style centered on resources and HTTP semantics. GraphQL commonly uses one HTTP endpoint, but its specification does not require a particular transport or endpoint layout.
What causes the N+1 problem in GraphQL?
N+1 occurs when a resolver loads a list and a nested resolver performs one data-source call for every item. It is caused by a naive fetching strategy, not by GraphQL itself. Batching, prefetching, joins, or a request-scoped DataLoader can reduce those calls.
What arguments does a GraphQL.js resolver receive?
A GraphQL.js resolver commonly receives source, args, context, and info. Source is the parent value, args contains field arguments, context carries request-scoped dependencies and identity, and info describes the operation and field. Other GraphQL implementations can expose different APIs.
What is the difference between Query and Mutation in GraphQL?
Query operations read data and their root fields may execute in parallel. Mutation operations represent writes, and the root mutation fields in one operation execute serially. That serialization does not prevent races between separate requests, so normal transaction and concurrency controls are still required.
Where should authentication and authorization happen in GraphQL?
Transport middleware or the context factory should authenticate credentials and pass a verified principal into request context. Authorization rules should have one trusted source of truth in the business or domain layer so they apply consistently to GraphQL, REST, jobs, and other entry points.
Should introspection be disabled in production?
It depends on the API and threat model. Disabling introspection can reduce discoverability for a first-party API, but it is only defense in depth and can hinder tooling. It never replaces authorization, safe error handling, demand controls, rate limits, or trusted documents.
Official Sources
- GraphQL Specification, September 2025
- GraphQL over HTTP working draft
- GraphQL security guidance
- GraphQL authorization guidance
- GraphQL subscriptions guidance
- DataLoader reference implementation
- Apollo Client cache documentation
- Apollo Client 4 migration guide
