Top 79 GraphQL Interview Questions & Answers (2026)
About GraphQL
Top 100 GraphQL interview questions covering queries, mutations, subscriptions, schema design, resolvers, performance, and advanced API design patterns. Companies hiring for GraphQL roles test this knowledge at every stage — from first-round screens that check the basics to final-round conversations about real-world trade-offs.
What to Expect in a GraphQL Interview
Expect a mix of conceptual and practical GraphQL questions: clear definitions and core-concept checks for junior roles, hands-on scenario questions for mid-level roles, and architecture or trade-off discussions for senior roles. Interviewers usually move from foundational topics toward the kind of problems you'd actually face on the job.
How to Use This Guide
Work through the GraphQL questions in order — Beginner, then Intermediate, then Advanced — so each concept builds on the last. Every question has its own page; bookmark the ones that trip you up and revisit them the day before your interview.
Curated by Tech Baithak Editorial Team · Last updated: June 2026
Core concepts every GraphQL developer must know.
01
What is GraphQL?
GraphQL is an open-source query language for APIs and a runtime for executing those queries, developed by Facebook in 2012 and open-sourced in 2015. Unlike REST APIs where the server determines the response shape, GraphQL lets the client specify exactly what data it needs — nothing more, nothing less. A single GraphQL endpoint replaces multiple REST endpoints. It is strongly typed: every API is described by a schema that defines types and their relationships. GraphQL supports three operation types: queries (read), mutations (write), and subscriptions (real-time). It is transport-agnostic but most commonly served over HTTP.
02
What are the main differences between GraphQL and REST?
REST uses multiple endpoints (one per resource), and the server controls the response shape — clients often receive too much data (over-fetching) or must make multiple requests for related data (under-fetching). GraphQL uses a single endpoint and lets the client request exactly the fields it needs. Key differences: (1) Over/under-fetching: GraphQL eliminates both. (2) Versioning: REST needs /v1, /v2 prefixes; GraphQL evolves the schema with deprecations. (3) Type system: GraphQL has a built-in strongly typed schema; REST relies on external documentation. (4) Discoverability: GraphQL has introspection; REST needs OpenAPI/Swagger. (5) Network requests: GraphQL can fetch nested data in one request; REST may require many. (6) Caching: REST has natural HTTP caching; GraphQL requires extra effort (persisted queries, CDN).
03
What is a GraphQL schema?
A GraphQL schema is the blueprint of an API — it defines all the types, fields, and operations available. Written in the Schema Definition Language (SDL), it serves as a contract between the client and server. The schema has three special root types: Query (for reads), Mutation (for writes), and Subscription (for real-time). Every field in the schema has a type, and types can be scalars (String, Int, Float, Boolean, ID) or object types (composed of fields). The schema is strongly typed and self-documenting via introspection. Example: type User { id: ID! name: String! email: String! }. The schema enforces what data can be queried and what operations are valid.
04
What is a GraphQL query?
A query is a read operation in GraphQL — the equivalent of an HTTP GET in REST. It retrieves data from the server without modifying anything. Queries specify exactly which fields to fetch. Example: query { user(id: "1") { name email posts { title } } } — this fetches the name, email, and post titles for user 1 in a single request. The response mirrors the query structure. Queries can be named (best practice): query GetUser($id: ID!) { user(id: $id) { name } }. Variables make queries reusable. Aliases let you rename fields: { admin: user(id: "1") { name } }. Queries can be deeply nested to fetch entire object graphs.
05
What is a GraphQL mutation?
A mutation is a write operation in GraphQL — it creates, updates, or deletes data (equivalent to POST/PUT/PATCH/DELETE in REST). Mutations are defined separately from queries in the schema under the Mutation root type. Example mutation definition: type Mutation { createUser(name: String!, email: String!): User! }. Calling it: mutation { createUser(name: "Alice", email: "alice@example.com") { id name } }. Like queries, mutations specify exactly which fields to return after the operation. With variables: mutation CreateUser($name: String!, $email: String!) { createUser(name: $name, email: $email) { id } }. Mutations execute serially (in order) when multiple are sent in one request, unlike query fields which resolve in parallel.
06
What is a GraphQL subscription?
A subscription is a real-time operation in GraphQL that maintains a persistent connection to the server and pushes data to the client when specific events occur. Unlike queries and mutations (request-response), subscriptions use WebSockets (or SSE) to stream updates. Example: subscription { messageAdded(channelId: "1") { id text author { name } } } — the client receives a new payload every time a message is added. Subscriptions are defined in the schema: type Subscription { messageAdded(channelId: ID!): Message! }. Server-side, subscriptions are powered by a pub/sub system (Redis, in-memory EventEmitter). Common use cases: chat apps, live dashboards, notifications, collaborative editing, and real-time data feeds.
07
What are GraphQL types?
GraphQL has a rich type system. Scalar types (leaf values): String, Int (32-bit signed), Float (64-bit), Boolean, ID (string representation of a unique identifier). Object types: custom types with fields: type Post { id: ID! title: String! }. Query/Mutation/Subscription: special root object types. Input types: object types used as arguments (not returned): input CreateUserInput { name: String! email: String! }. Enum types: fixed value sets: enum Status { ACTIVE INACTIVE BANNED }. Interface types: abstract shared fields. Union types: a field that can return multiple types. List types: [String]. Non-null modifier: ! — e.g., String! means the field can never return null.
08
What is the difference between ! (non-null) and nullable fields in GraphQL?
In GraphQL, by default, all fields are nullable — a field can return null if the value is absent. Adding ! makes a field non-null — the server guarantees it will never return null. If a non-null resolver throws or returns null, GraphQL propagates the null error up to the nearest nullable parent. Examples: name: String (may be null), name: String! (never null), tags: [String] (list may be null), tags: [String!] (list not null but items may be null), tags: [String]! (list not null, items may be null), tags: [String!]! (both list and items are non-null). Best practice: be conservative — mark fields non-null only when you truly guarantee non-null values, as changing non-null to nullable is a breaking change.
09
What is a GraphQL resolver?
A resolver is a function on the server that fetches the data for a specific field in the schema. Every field in a GraphQL schema has a corresponding resolver. Resolvers have the signature: fieldName(parent, args, context, info). parent (or root): the resolved value of the parent object. args: the arguments passed to the field in the query. context: shared across all resolvers in a request — typically contains the authenticated user, database connection, and data loaders. info: field-specific metadata about the query (AST, schema, path). If a resolver is not defined, GraphQL uses a default resolver that reads the property of the same name from the parent object. Resolvers can be async and return Promises.
10
What is the GraphQL Schema Definition Language (SDL)?
The Schema Definition Language (SDL) is the human-readable syntax for writing GraphQL schemas. It is language-agnostic and used to describe the type system. Key constructs: type TypeName { field: Type } (object type), interface InterfaceName { field: Type }, union UnionName = TypeA | TypeB, enum EnumName { VALUE1 VALUE2 }, input InputName { field: Type } (for mutation arguments), scalar CustomScalar, directive @directiveName on FIELD_DEFINITION. Comments use # (inline) or triple-quote strings """Description""" for documentation. The SDL is used by tools like Apollo Studio, GraphiQL, and code generators to provide documentation, type checking, and client code generation.
11
What is GraphQL introspection?
Introspection is GraphQL's built-in ability to query the schema itself — to discover what types, fields, and operations are available. Special introspection queries start with __: { __schema { types { name } } } lists all types. { __type(name: "User") { fields { name type { name } } } } describes a specific type. { __typename } returns the type name of the current object. Tools like GraphiQL, Apollo Sandbox, and Insomnia use introspection to provide autocomplete and documentation. Code generators (GraphQL Code Generator) use introspection to generate TypeScript types. Important: disable introspection in production environments to prevent API schema exposure to attackers — many GraphQL security issues start with schema enumeration.
12
What is GraphiQL?
GraphiQL is the official in-browser IDE for GraphQL — an interactive playground where you can write, validate, and execute GraphQL queries against a server. It is typically served by the GraphQL server at the same endpoint (e.g., /graphql) and uses introspection to provide: autocomplete for fields and types, syntax highlighting, inline documentation, query history, and variable and header editing. A simple query: type { users { id name } } and press Ctrl+Enter. Apollo Sandbox is a more modern alternative that works without a local server. GraphQL Playground was another popular option (now merged into Apollo Sandbox). All use introspection under the hood and make GraphQL API exploration significantly easier than REST.
13
What are GraphQL variables?
Variables make GraphQL operations dynamic and reusable by externalizing dynamic values from the query string. Instead of inline values: { user(id: "123") { name } }, use variables: query GetUser($id: ID!) { user(id: $id) { name } } — then pass { "id": "123" } as a separate JSON object. Variables are declared in the operation signature with $name: Type syntax. Benefits: (1) Prevents injection attacks (values are never embedded in the query string). (2) Makes operations reusable with different inputs. (3) GraphQL clients (Apollo, Relay) can cache by operation name + variable hash. Variable defaults: $status: Status = ACTIVE. Variables must match the declared type — the server validates them before execution.
14
What are GraphQL fragments?
Fragments are reusable units of fields that can be included in multiple operations, reducing duplication. Define a fragment: fragment UserFields on User { id name email avatar }. Use with spread operator ...: query { user(id: "1") { ...UserFields posts { title } } }. Benefits: (1) DRY principle — define shared field sets once. (2) Co-location: UI components (in React + Apollo) can declare their own data fragments and compose them. (3) Inline fragments: used for type conditions in unions/interfaces: { search { ... on User { name } ... on Post { title } } }. Fragments must be used on a specific type (on TypeName). Named fragments are sent to the server; the server merges them into the operation before execution.
15
What are inline fragments in GraphQL?
Inline fragments allow you to query fields on a specific concrete type within a union or interface. Syntax: ... on TypeName { fields }. Example with a union: query { search(term: "GraphQL") { ... on Book { title author } ... on Article { headline publication } } }. If the result is a Book, title and author are returned; if an Article, headline and publication. Use __typename to determine the concrete type at runtime: { search { __typename ... on Book { title } } }. Inline fragments are also used without a type condition as a way to apply directives to a group of fields: ... @include(if: $showDetails) { createdAt updatedAt }.
16
What are GraphQL directives?
Directives modify the behavior of fields or types at execution time. Built-in directives: @include(if: Boolean) — include a field only if the condition is true; @skip(if: Boolean) — skip a field if the condition is true; @deprecated(reason: "Use newField instead") — marks a field as deprecated in the schema; @specifiedBy(url: "...") — for custom scalar documentation. Usage: query GetUser($showEmail: Boolean!) { user(id: "1") { name email @include(if: $showEmail) } }. Servers can define custom directives for cross-cutting concerns like authentication (@auth), rate limiting (@rateLimit), caching (@cacheControl), or computed fields. Schema directives are applied at the SDL level; execution directives are used in queries.
17
What is the difference between a query and a mutation in terms of execution?
The key execution difference is ordering: query fields resolve in parallel (by default), while mutation fields resolve serially (one after another in order). This is intentional — mutations may have side effects that depend on each other, and sequential execution ensures predictability. Example: sending two mutations mutation { deleteUser(id: "1") updateUser(id: "1", name: "X") } executes delete first, then update. If both were parallel, the order would be undefined. Queries are assumed to be pure (no side effects), so the executor can fetch all fields concurrently for better performance. In practice, individual resolvers within a single mutation are still executed — it's only the top-level mutation fields that are sequential. Both return data in the response following the same structure.
18
What is the N+1 problem in GraphQL?
The N+1 problem occurs when fetching a list of N items triggers N additional database queries to fetch related data — one per item. Example: fetching 100 posts, then fetching the author for each post separately results in 1 + 100 = 101 database queries. In GraphQL, this happens when a resolver for a nested field (like post.author) makes a database call per parent: resolve: (post) => db.user.findById(post.authorId). With 100 posts in the list, this fires 100 separate queries. The solution is DataLoader — a batching and caching utility that collects all individual requests within a single tick of the event loop, then makes a single batched query: SELECT * FROM users WHERE id IN (1, 2, 3, ...). This is one of the most critical performance patterns in any GraphQL server.
19
What is DataLoader and how does it solve the N+1 problem?
DataLoader is a utility (originally from Facebook, now open-source) that batches and caches database requests within a single request/tick cycle. It works in two phases: (1) Batching: during a single event loop tick, all calls to dataloader.load(id) are collected; at the end of the tick, the batch function is called once with all collected IDs: async (ids) => db.users.findAll({ where: { id: ids } }) — one query for all. (2) Caching: within a single request, loading the same ID twice returns the cached result. Example: const userLoader = new DataLoader(ids => UserModel.findAll(ids));. In resolvers: resolve: (post) => context.loaders.user.load(post.authorId). DataLoader is per-request (not global) to ensure cache isolation between users. It is the standard solution for N+1 in GraphQL servers.
20
What is the Apollo Client?
Apollo Client is a comprehensive state management library for JavaScript that integrates with GraphQL. It handles: (1) Fetching: sending queries and mutations to a GraphQL server. (2) Caching: a normalized in-memory cache that automatically stores query results and updates UI when data changes. (3) Local state: managing local application state alongside server data. (4) React integration: hooks like useQuery, useMutation, useSubscription. (5) Optimistic UI: immediately updating the UI before the server responds. (6) Pagination: cursor-based and offset pagination helpers. Setup: wrap your app in ApolloProvider with an ApolloClient instance. Apollo Client works with React, Angular, Vue, and plain JavaScript. Alternatives include urql (lighter) and Relay (more opinionated).
21
What is Apollo Server?
Apollo Server is a popular open-source GraphQL server library for Node.js that makes it easy to build a GraphQL API. It handles: parsing queries, validating against the schema, executing resolvers, and formatting the response. It integrates with Express, Fastify, Koa, and serverless environments. Key features: built-in GraphQL Playground/Sandbox, plugin system, performance monitoring (Apollo Studio integration), schema stitching and federation support. Basic setup: const server = new ApolloServer({ typeDefs, resolvers }); server.listen(4000);. In v4 (current), it is framework-agnostic and uses the startStandaloneServer or expressMiddleware functions. Alternatives: Yoga (The Guild), Mercurius (Fastify-native), GraphQL.js (reference implementation), Pothos, NestJS GraphQL module.
22
What is a GraphQL endpoint?
A GraphQL endpoint is a single URL that handles all API operations — unlike REST which has a different URL per resource. Conventionally, it is served at /graphql (e.g., https://api.example.com/graphql). All queries, mutations, and subscriptions go to this one endpoint. HTTP method: POST for queries and mutations (body contains the operation), GET is supported for queries (query string parameter). Request body format: { "query": "{ users { id name } }", "variables": {}, "operationName": "GetUsers" }. Response is always JSON: { "data": {...}, "errors": [...] }. Subscriptions typically use a WebSocket connection to the same or a separate endpoint. The single endpoint simplifies API versioning but requires more careful authorization and rate limiting.
23
What is the GraphQL response format?
GraphQL responses always follow a standardized JSON format regardless of success or failure. The response object has up to three top-level fields: (1) data: the result of the operation — mirrors the query structure exactly. Present on success; may be null if a non-null field errored. (2) errors: an array of error objects when something goes wrong. Each error has: message (required), locations (query position), path (path to the errored field), extensions (custom metadata like error codes). Critically, GraphQL can return both data and errors simultaneously — partial success is possible (some fields succeed, others fail). (3) extensions: optional metadata (tracing info, cache hints). HTTP status is almost always 200 even for errors — clients must check the errors array.
24
What are enum types in GraphQL?
Enum types define a set of allowed constant values for a field. They restrict a field to one of a predefined set of string values. Definition: enum UserRole { ADMIN EDITOR VIEWER }. Usage in a type: type User { id: ID! role: UserRole! }. In a query response, the value comes back as the string "ADMIN". In mutations: mutation { updateUser(id: "1", role: ADMIN) { role } } — note: enum values in queries are passed without quotes (unlike strings). Enums are useful for: status fields, roles, categories, directions. When an invalid enum value is passed, GraphQL validation rejects the request before execution. Enum values are all-caps by convention in GraphQL (though not enforced).
25
What are input types in GraphQL?
Input types are special object types used exclusively as arguments for mutations (and queries). They cannot be used as output types. They are defined with the input keyword: input CreatePostInput { title: String! content: String! authorId: ID! tags: [String!] }. Usage: type Mutation { createPost(input: CreatePostInput!): Post! }. Calling it: mutation { createPost(input: { title: "Hello", content: "World", authorId: "1" }) { id } }. Input types help organize complex argument lists into a single object, making the API cleaner. They can be nested: an input type can have fields of other input types. Unlike object types, input types cannot have fields that resolve to interfaces, unions, or regular object types.
26
What are interfaces in GraphQL?
Interfaces define a set of fields that implementing types must include. They are used to express shared structure across multiple types. Definition: interface Node { id: ID! }. Implementation: type User implements Node { id: ID! name: String! } type Post implements Node { id: ID! title: String! }. In queries, you can query interface fields directly and use inline fragments for type-specific fields: query { node(id: "1") { id ... on User { name } ... on Post { title } } }. Resolvers must implement a __resolveType function to determine the concrete type at runtime. A type can implement multiple interfaces: type Article implements Node & Searchable. Interfaces promote reuse and allow generic patterns like the Node interface (Relay global object identification).
27
What are union types in GraphQL?
Union types define a field that can return one of several different object types, without requiring shared fields. Definition: union SearchResult = User | Post | Comment. Usage: type Query { search(term: String!): [SearchResult!]! }. Unlike interfaces, union members don't share any fields — you must use inline fragments for all fields: { search(term: "Alice") { ... on User { name } ... on Post { title } ... on Comment { text } } }. The server must define a __resolveType function to determine which concrete type to return. Use __typename to identify the type at runtime. Unions are ideal for: search results, notification feeds, event logs, or any field that can legitimately return different object types. Interfaces are preferred when types share common fields.
28
What is schema-first vs code-first GraphQL development?
These are two approaches to building a GraphQL schema. Schema-first (SDL-first): you write the schema in SDL (.graphql files) first, then implement resolvers to match it. Tools: Apollo Server, graphql-tools, Nexus (with SDL input). Benefits: schema is the single source of truth, great for API design reviews, language-agnostic. Drawback: schema and resolver code can drift if not kept in sync. Code-first: you define the schema programmatically in code, and the SDL is generated from it. Tools: Nexus, TypeGraphQL, Pothos, Hot Chocolate. Benefits: type safety (TypeScript), no duplication between code and schema, IDE support. Drawback: generated SDL can be less readable. Which to choose: schema-first is great for teams doing design-first API development; code-first is preferred for TypeScript teams wanting end-to-end type safety.
29
What is the context object in GraphQL?
The context is an object shared across all resolvers in a single request. It is the third argument to every resolver: resolver(parent, args, context, info). Typical contents: authenticated user (from JWT/session), database connection or ORM instance, DataLoader instances (created per-request for batching), service clients, request headers, logger. The context is created once per request in the server configuration: context: ({ req }) => ({ user: getUser(req.headers.authorization), db, loaders: createLoaders() }). Because it's shared, resolvers can access user info and data sources without passing them as arguments. Important: DataLoaders must be created per-request (not shared globally) to prevent cache leakage between requests from different users.
30
What is the root value in GraphQL?
The root value (sometimes called rootValue) is an optional object passed to the GraphQL executor that serves as the parent object for top-level resolver functions. In the reference GraphQL.js implementation, it's the value passed as the second argument to graphql(schema, query, rootValue). For Query resolvers, parent (first resolver argument) is the root value. In practice, most modern GraphQL frameworks (Apollo Server, Yoga) do not rely on root value — instead, they use the context for shared data and define resolvers on the Query/Mutation types directly. The root value pattern is more common in simple setups or the reference implementation. For production apps, the context pattern is preferred as it's more flexible and explicit.
31
How does error handling work in GraphQL?
GraphQL has a unique error model. When a resolver throws an error, GraphQL catches it and adds it to the errors array in the response — the rest of the query continues executing (partial results). If the errored field is non-null (!), GraphQL propagates the null upward to the nearest nullable parent. The response can have both data and errors simultaneously. Error objects contain: message, locations (query position), path (field path), and extensions (custom data). In Apollo Server, throw new GraphQLError('message', { extensions: { code: 'NOT_FOUND' } }). Common error codes: UNAUTHENTICATED, FORBIDDEN, NOT_FOUND, BAD_USER_INPUT, INTERNAL_SERVER_ERROR. Never expose raw database errors to clients — mask internal errors in production.
32
What is the info argument in a GraphQL resolver?
The info object (fourth resolver argument) contains metadata about the current execution context. Key properties: fieldName (name of the current field), fieldNodes (AST nodes for the current field — contains the sub-selection of requested fields), returnType (the GraphQL type being returned), parentType (the type the field belongs to), schema (the full GraphQL schema), path (path from root to current field), rootValue, operation (the full query AST). Advanced use cases: inspecting which sub-fields are requested to optimize database queries (only SELECT needed columns), implementing custom caching based on types, and building field-level middleware. Libraries like graphql-fields parse the info object to extract requested fields easily.
33
What is persisted queries in GraphQL?
Persisted queries are a performance and security optimization where query strings are stored on the server and clients send only a hash instead of the full query text. Instead of sending { "query": "{ users { id name email ... } }" }, the client sends { "extensions": { "persistedQuery": { "version": 1, "sha256Hash": "abc123..." } } }. Benefits: (1) Smaller requests — just a hash instead of potentially kilobytes of query text. (2) Better caching — queries can be GET-requested and cached by CDNs. (3) Security — with Automatic Persisted Queries (APQ) or whitelisting, only pre-approved queries can execute, blocking arbitrary query attacks. Apollo Server supports APQ out of the box. Trusted document queries (previously called safelisted/whitelisted queries) only allow pre-registered operations — maximum security for production.
34
What are custom scalars in GraphQL?
GraphQL's built-in scalars (String, Int, Float, Boolean, ID) are limited. Custom scalars extend the type system for specialized value types. Common examples: scalar Date, scalar DateTime, scalar JSON, scalar URL, scalar EmailAddress, scalar UUID, scalar Upload (file uploads). Define in SDL: scalar DateTime. Implement with serialize (output → JSON), parseValue (variable input → internal), and parseLiteral (query literal → internal) functions. The graphql-scalars library provides production-ready implementations for 50+ common scalars. Example: a Date scalar can accept "2024-01-15" as input and return JavaScript Date objects to resolvers, while serializing to ISO strings in responses.
35
What is GraphQL Federation?
GraphQL Federation (by Apollo) is an architecture for building a distributed GraphQL API where multiple independent services (subgraphs) each own part of the schema, and a gateway/router composes them into a single unified supergraph. Each subgraph defines its own types and resolvers; the router merges schemas and routes query fields to the appropriate service. Key concepts: @key directive (entity primary key for cross-service references), @external (field defined in another service), @requires (depends on a field from the same entity), @extends (extend a type from another service). Example: a User type defined in the Users service can be extended by the Orders service to add orders field. This enables teams to work independently while clients see one unified graph. Federation v2 removed many of the v1 limitations.
36
What is the difference between query, mutation, and subscription operations?
Query: a read-only operation that fetches data without causing side effects. Resolvers are called in parallel. Used for GET-like operations — reading user profiles, lists, search results. Mutation: a write operation that modifies server-side data. Top-level mutation fields execute serially. Used for creating, updating, or deleting data — login, creating a post, purchasing an item. Subscription: a long-lived operation that establishes a connection (typically WebSocket) and receives server-pushed events when data changes. Used for real-time features — chat messages, live notifications, dashboard updates. All three are defined in the root types type Query, type Mutation, type Subscription. If the operation keyword is omitted, GraphQL assumes it's a Query (shorthand). Best practice: always include the operation keyword for clarity.
37
How do you handle file uploads in GraphQL?
GraphQL doesn't natively support file uploads since it is typically JSON-based. The most common approach is the GraphQL multipart request spec, implemented by libraries like graphql-upload. This uses a multipart form-data request to send files alongside the query. The Upload scalar is added to the schema: scalar Upload. In a mutation: mutation ($file: Upload!) { uploadFile(file: $file) { filename } }. The resolver receives a Promise resolving to an object with createReadStream, filename, mimetype. Alternative approaches: (1) Use a pre-signed URL from S3/GCS — return a URL from a mutation, then upload directly to storage from the client (preferred for large files). (2) Upload to a separate REST endpoint and pass the URL to GraphQL. Apollo Client supports the multipart spec via @apollo/client + apollo-upload-client.
38
What is the difference between REST and GraphQL in terms of over-fetching and under-fetching?
Over-fetching means receiving more data than needed. In REST, a GET /users/1 endpoint returns the full user object with all fields, even if the client only needs the name and avatar. With hundreds of fields, this wastes bandwidth. GraphQL eliminates over-fetching by letting clients specify exactly which fields to return: { user(id: "1") { name avatar } }. Under-fetching means not receiving enough data in one request, requiring additional requests. In REST, fetching a user's profile, their posts, and each post's comments might require: GET /users/1, then GET /users/1/posts, then GET /posts/1/comments, etc. — multiple round trips. GraphQL fetches all of this in a single request: { user(id: "1") { name posts { title comments { text } } } }. This is particularly valuable for mobile clients where network round trips are expensive.
39
How do you authenticate in GraphQL?
GraphQL doesn't have built-in authentication — it delegates to the application layer. Common patterns: (1) HTTP headers: send a JWT in the Authorization: Bearer TOKEN header with every request. The GraphQL server extracts and verifies the token in the context function, making the user object available to all resolvers. (2) Session cookies: work the same as with REST APIs — the browser sends cookies automatically. (3) OAuth/OIDC: implement standard OAuth flows with redirect; the resulting access token is used in Authorization header. In resolvers, check context.user: if (!context.user) throw new GraphQLError('Unauthenticated', { extensions: { code: 'UNAUTHENTICATED' } });. Never put authentication logic in individual resolvers repeatedly — use schema directives (@auth) or middleware for DRY authentication.
40
What is the args argument in a GraphQL resolver?
The args object (second resolver argument) contains all the arguments passed to the field in the query. Arguments are defined in the schema: user(id: ID!): User. In the resolver: Query: { user: (parent, args, context) => db.user.findById(args.id) }. Arguments can be scalars, enums, or input types. For a mutation: createUser(input: CreateUserInput!): User → resolver receives args.input.name, args.input.email. Arguments are validated by GraphQL before reaching the resolver — if a required argument is missing or the wrong type, execution fails before the resolver is called. Default values in schema: field(limit: Int = 10). Args are useful for filtering, pagination, IDs, and any operation parameter.
Practical knowledge for developers with hands-on experience.
01
How do you implement pagination in GraphQL?
Two main pagination styles in GraphQL: Offset/limit pagination: simple but has problems at high offsets. query { users(limit: 20, offset: 40) { id name } }. Cursor-based (Relay-style) pagination: the recommended approach. Uses opaque cursors (base64-encoded IDs or timestamps) as pointers into the dataset. Schema: type UserConnection { edges: [UserEdge!]! pageInfo: PageInfo! } type UserEdge { cursor: String! node: User! } type PageInfo { hasNextPage: Boolean! hasPreviousPage: Boolean! startCursor: String endCursor: String }. Query: { users(first: 20, after: "cursor123") { edges { node { name } cursor } pageInfo { hasNextPage endCursor } } }. Cursor pagination is stable (no items missed/duplicated when data changes) and scales to any dataset size. Libraries like prisma-relay-cursor-connection implement the Relay Cursor Connections spec.
02
What is the Relay specification in GraphQL?
The Relay specification is a set of conventions developed by Facebook/Meta for structuring GraphQL APIs to work optimally with the Relay client (but adopted broadly). Key conventions: (1) Global Object Identification: every object has a globally unique id: ID! field, and the root query has a node(id: ID!): Node field to fetch any object by ID. (2) Cursor Connections: a standardized pagination pattern using Connection, Edge, and PageInfo types. (3) Mutation conventions: mutations accept a single input argument and return a payload object. Following the Relay spec makes APIs more consistent, enables powerful client-side caching, and allows Relay's automatic store normalization. Even when not using Relay client, following these conventions makes your API more standardized and tooling-friendly.
03
How do you implement authorization in GraphQL resolvers?
Authorization (what an authenticated user can do) in GraphQL can be implemented at multiple levels: (1) Resolver-level: check permissions in each resolver — if (context.user.role !== 'ADMIN') throw new GraphQLError('Forbidden', { extensions: { code: 'FORBIDDEN' } });. Verbose but granular. (2) Schema directives: @auth(requires: ADMIN) declarative annotation on fields — cleaner but requires directive implementation. (3) Middleware: wrap resolvers with permission checks using graphql-shield — define rules declaratively outside resolvers. (4) Business logic layer: move authorization to service/model classes, keeping resolvers thin. (5) Row-level security: push authorization to the database layer (PostgreSQL RLS). Best practice: use a service layer where objects are pre-filtered by user permissions — never trust client IDs without verifying ownership.
04
What is graphql-shield and how does it work?
graphql-shield is a permission middleware library for GraphQL servers that lets you define authorization rules declaratively, separate from resolver logic. It uses a rule-based system where you define rules using rule() and apply them to types/fields using shield(). Rules can be composable with and(), or(), not(). Example: const isAuthenticated = rule()((parent, args, ctx) => ctx.user !== null); const isAdmin = rule()((parent, args, ctx) => ctx.user?.role === 'ADMIN'); const permissions = shield({ Query: { users: isAuthenticated, adminDashboard: isAdmin } });. Rules are cached per field by default (to avoid redundant checks). Shield integrates with Apollo Server as a schema transformation. It provides a clean separation of authorization concerns from business logic, making permission models easier to audit and maintain.
05
How do you implement real-time subscriptions with GraphQL?
GraphQL subscriptions require: (1) A WebSocket transport — the standard protocol is graphql-ws (newer, preferred over the older subscriptions-transport-ws). (2) A pub/sub system to broadcast events. In-memory EventEmitter for single-server; Redis pub/sub for multi-server deployments. Apollo Server setup: configure WebSocketServer alongside the HTTP server; use useServer from graphql-ws. Subscription resolver structure: subscribe function (returns an AsyncIterator/AsyncGenerator that yields events) and resolve function (transforms the event into the response shape). Example: Subscription: { messageAdded: { subscribe: (_, { channelId }, { pubsub }) => pubsub.asyncIterableIterator(['MESSAGE_ADDED_' + channelId]), resolve: (payload) => payload.message } }. Trigger from mutations: pubsub.publish('MESSAGE_ADDED_1', { message: newMessage });.
06
What is schema stitching in GraphQL?
Schema stitching is the process of combining multiple separate GraphQL schemas into one unified schema. Implemented by the @graphql-tools/stitch library. It merges type definitions and resolvers from multiple schemas, enabling a gateway pattern where one GraphQL server aggregates several downstream services. Key features: type merging (combining the same type from multiple services), transforms (rename types/fields to avoid conflicts), remote schemas (proxy requests to remote GraphQL endpoints). Stitching differs from Federation: stitching is implemented in the gateway's Node.js code; Federation uses a spec-compliant protocol between services. Schema stitching is more flexible but requires more manual configuration. For new projects, Apollo Federation is generally preferred; stitching is still useful for integrating legacy or third-party GraphQL APIs.
07
What is a GraphQL gateway?
A GraphQL gateway is a server that acts as the single entry point for clients, aggregating data from multiple downstream GraphQL services (or REST/gRPC services) and presenting a unified schema. The gateway handles: schema composition (merging subgraphs), query planning (splitting a query across multiple services), result merging (combining responses into one), cross-cutting concerns (auth, rate limiting, logging). Apollo Router is the production-grade gateway for Federation v2 (written in Rust for performance). GraphQL Mesh can serve as a gateway wrapping REST, gRPC, SOAP, and GraphQL services. The gateway pattern enables micro-frontend/microservice architectures where each team owns their service but clients see one clean API. The query planner optimizes which services to call and in what order based on field dependencies.
08
What is the Apollo Cache and how does normalized caching work?
Apollo Client's InMemoryCache uses a normalized cache that stores objects by their unique identity, not by query. When a query result is received, every object with an id field is flattened and stored under a cache key like User:42. Subsequent queries or mutations that return the same user (by ID) automatically update the same cache entry — UI components that display that user update automatically. This means: if you update User 42's name via a mutation that returns the updated user, all queries referencing User 42 reflect the change without refetching. Cache behavior is configured via typePolicies: define custom key fields, merge functions for paginated lists, and read functions for computed fields. The keyFields option specifies which fields to use for identity (default: ['__typename', 'id']).
09
How do you handle optimistic UI in GraphQL with Apollo?
Optimistic UI means updating the UI immediately with an expected result before the server confirms the mutation — making the app feel faster and more responsive. In Apollo Client, pass an optimisticResponse option to useMutation: const [addTodo] = useMutation(ADD_TODO, { optimisticResponse: { addTodo: { __typename: 'Todo', id: 'temp-id', text: newText, completed: false } }, update(cache, { data: { addTodo } }) { cache.modify({ fields: { todos(existing) { return [...existing, makeReference(cache.identify(addTodo))]; } } }); } });. Apollo writes the optimistic response to the cache immediately, the UI re-renders, and when the real response arrives, the optimistic entry is replaced. If the mutation fails, the optimistic update is rolled back. Works well for likes, upvotes, adding list items, and other low-risk operations.
10
What are Apollo Client hooks and how do you use them?
Apollo Client v3 provides React hooks for all GraphQL operations. useQuery: const { data, loading, error, refetch } = useQuery(GET_USERS, { variables: { status: 'active' }, skip: !isLoggedIn, pollInterval: 5000 });. Returns data, loading state, error, and helpers. useMutation: const [createUser, { loading, error }] = useMutation(CREATE_USER, { onCompleted: (data) => navigate('/users/' + data.createUser.id), update(cache, { data }) { /* update cache manually */ } });. Call it with: createUser({ variables: { name: 'Alice' } });. useSubscription: const { data } = useSubscription(MESSAGE_ADDED, { variables: { channelId } });. useLazyQuery: returns a function to trigger the query manually (not on mount). useApolloClient: access the client directly for cache manipulation.
11
What is the difference between network-only, cache-first, and cache-and-network fetch policies?
Apollo Client's fetch policies control how queries interact with the cache. cache-first (default): read from cache; only fetch from network if data is not cached. Best for stable data. network-only: always fetch from network; result is stored in cache for future queries but cache is never used for the current query. Best for frequently changing critical data. cache-and-network: returns cached data immediately (fast render), then fetches from network and updates if data changed. Good balance for most use cases. no-cache: always fetch from network; result is NOT stored in cache. For sensitive/one-off queries. cache-only: only read from cache; error if not cached. For offline-first apps. standby: like cache-first but doesn't update on cache changes. Set per query: useQuery(QUERY, { fetchPolicy: 'network-only' }).
12
How do you update the Apollo cache after a mutation?
When a mutation creates or deletes items (not just updates), the Apollo cache doesn't automatically know to update list queries. Three approaches: (1) Refetch queries: simplest — refetch affected queries after mutation: useMutation(ADD_TODO, { refetchQueries: [GET_TODOS] }). Causes an extra network request. (2) Cache update function: manually update the cache: update(cache, { data: { addTodo } }) { cache.modify({ fields: { todos(existing) { return [...existing, cache.writeFragment({ data: addTodo, fragment: TODO_FIELDS })]; } } }); }. (3) Cache invalidation: evict specific cache entries: cache.evict({ id: cache.identify(deletedItem) }); cache.gc();. The cache.modify approach is most fine-grained. For complex pagination, the mergeFunction in typePolicies handles automatic merging.
13
What is the graphql-tag (gql) utility?
gql is a tagged template literal function from the graphql-tag package (also exported from @apollo/client) that parses GraphQL query strings into AST document nodes. Usage: const GET_USER = gql\`query GetUser($id: ID!) { user(id: $id) { name email } }\`;. Benefits: (1) Compile-time parsing: the query is parsed once when the module loads, not on every render. (2) Syntax validation: IDEs with GraphQL plugins highlight errors in gql template literals. (3) Fragment composition: gql\`query { ...${USER_FIELDS} }\` for fragment spreading. (4) Code generation: GraphQL Code Generator uses gql-tagged queries to generate TypeScript types. (5) Tooling support: Apollo DevTools recognizes gql-parsed queries. Store queries in .graphql files (better IDE support) and import them, or use the tagged template approach inline.
14
What is query depth limiting in GraphQL and why is it important?
Query depth limiting is a security measure that restricts how deeply nested a GraphQL query can be. Without it, a malicious actor can craft deeply nested queries that cause exponential data fetching: { users { friends { friends { friends { friends { ... } } } } } }. Each level multiplies the number of database calls. The graphql-depth-limit package adds a validation rule: depthLimit(7) rejects queries deeper than 7 levels. This should be combined with other protections: query complexity analysis (assign a cost to each field and reject queries exceeding a budget), query timeout, and rate limiting. The maximum safe depth depends on your schema — typically 5-10 levels. In production, always implement at minimum: depth limiting + complexity limiting + authentication rate limits. Disable introspection and field suggestions in production to reduce attack surface.
15
What is query complexity analysis in GraphQL?
Query complexity analysis assigns a cost to each field and rejects queries whose total cost exceeds a configured threshold, preventing expensive queries from overwhelming the server. Implemented by graphql-query-complexity. Each field gets a complexity value (default: 1); list fields get a multiplier based on the requested limit. Example: complexity: (args, childComplexity) => args.limit * childComplexity — fetching 100 users each with 10 posts costs 100 * 10 = 1000. If the threshold is 500, this query is rejected. Define field-level complexity in the schema or resolver map. Integration: add as a validation rule to the GraphQL server. Pair with depth limiting, rate limiting, and timeout for comprehensive DoS protection. Apollo Studio's operation metrics can help tune complexity budgets by analyzing real query costs in production.
16
What is the difference between Apollo Server v3 and v4?
Apollo Server v4 (released 2022) introduced major architectural changes from v3. Key differences: (1) Framework integration: v4 is framework-agnostic — no built-in Express integration. Use startStandaloneServer for simple setups or expressMiddleware for Express integration (separate package). (2) Removed built-in landing page: no embedded GraphQL Playground; replaced by Apollo Sandbox link. (3) Plugin system overhaul: cleaner hook-based plugin API. (4) Built-in TypeScript: written in TypeScript, better type inference. (5) No more subscriptions integration: subscriptions must be set up separately with graphql-ws. (6) CSRF prevention by default. (7) HTTP batching removed by default. (8) Better error handling: error formatting is more controlled. Migration from v3 to v4 requires updating server initialization and subscription setup.
17
What is urql and how does it compare to Apollo Client?
urql (Universal React Query Library) is a lightweight, extensible GraphQL client for React, Svelte, Vue, and vanilla JS. Comparison with Apollo Client: Size: urql is ~5KB vs Apollo Client's ~30KB+ — significant for bundle-size-sensitive apps. Caching: urql uses document-based caching by default (cache entire query results) vs Apollo's normalized cache. urql also has an optional Graphcache plugin for normalized caching. Simplicity: urql has a simpler API and less configuration needed. Flexibility: urql uses an "exchanges" pipeline (middleware system) that is highly composable — auth, retry, offline, subscriptions are all exchanges. Ecosystem: Apollo Client has a larger ecosystem, better Apollo Studio integration, and more community resources. When to choose urql: bundle size matters, simpler caching requirements, or preference for a more modular architecture.
18
What is Relay and how does it differ from Apollo Client?
Relay is Meta's production-grade GraphQL client for React, used at Facebook scale. Key differences from Apollo Client: (1) Co-location: Relay enforces that each component declares its own data requirements via fragments — mandatory, not optional. Components compose their fragments upward. (2) Compiler: Relay has a build-time compiler that validates fragments, generates optimized query artifacts, and creates TypeScript types — catches schema mismatches at build time. (3) Strict spec adherence: Relay requires the server to follow the Relay spec (global object IDs, cursor connections) — no flexibility. (4) Performance: Relay is optimized for massive data needs — incremental delivery, streaming, deferred fragments. (5) Learning curve: Relay is significantly more complex to set up and learn. (6) When to choose Relay: large-scale React apps with many components and complex data requirements. Apollo is easier to adopt and more flexible.
19
What is the GraphQL over HTTP spec?
The GraphQL over HTTP specification formalizes how GraphQL requests and responses should be transported over HTTP, bringing consistency across different server implementations. Key requirements: (1) POST requests: body must be JSON with query, optional variables and operationName. Content-Type: application/json. (2) GET requests: supported for queries only — parameters in query string. (3) Response format: must have Content-Type: application/graphql-response+json (new media type) for compliant implementations. (4) Status codes: 200 for success + partial errors, 400 for invalid queries, 401/403 for auth errors. (5) Application/json: for backwards compatibility. The spec (maintained by The Guild and community) allows clients to reliably work with any compliant server without server-specific adapters. GraphQL Yoga follows this spec strictly.
20
How do you implement a DataLoader with batching and caching?
DataLoader batches and caches within a single request. Implementation: import DataLoader from 'dataloader';. Define a batch function: const batchUsers = async (ids) => { const users = await User.findAll({ where: { id: ids } }); return ids.map(id => users.find(u => u.id === id) || new Error(`User ${id} not found`)); };. Note: the batch function must return an array in the same order as the input IDs array — this is critical. Create per-request: const userLoader = new DataLoader(batchUsers);. Use: const user = await userLoader.load(userId);. Multiple loads in one tick are batched: const [u1, u2] = await Promise.all([userLoader.load(1), userLoader.load(2)]) → one DB query. Cache: userLoader.load(1) called twice returns the same Promise. Disable cache for mutations: userLoader.clear(userId) after update. Create loaders in the context factory so each request gets fresh instances.
21
What are Apollo Client's reactive variables?
Reactive variables are Apollo Client's mechanism for managing local state that automatically triggers React re-renders and can be read in queries. Define: const isLoggedInVar = makeVar(false);. Read: const isLoggedIn = useReactiveVar(isLoggedInVar);. Write: isLoggedInVar(true); — any component using useReactiveVar(isLoggedInVar) automatically re-renders. Integrate with cache queries via typePolicies: Query: { isLoggedIn: { read() { return isLoggedInVar(); } } } — then query { isLoggedIn @client }. Reactive variables replace the need for Redux or Zustand for simple global state when using Apollo Client. They are simpler than field policies but equally reactive. Use for: auth state, UI state (modal open/closed), filters, shopping cart contents.
22
What is the @client directive in Apollo Client?
The @client directive marks a field as a local-only field that should be resolved from the Apollo Client cache rather than sent to the server. This allows mixing server data with client-side local state in a single query. Example: const GET_USER_WITH_UI = gql\` query { user(id: "1") { name email } isCartOpen @client sidebarWidth @client }\`; — isCartOpen and sidebarWidth are resolved locally using field read policies or reactive variables. Local fields can be combined with remote fields seamlessly. Write local data: client.writeQuery({ query: LOCAL_STATE, data: { isCartOpen: true } }) or use cache.modify. The @client directive is excluded from the network request — the server never sees it. This is Apollo's built-in approach to local state management.
23
What is deferred query execution in GraphQL?
Deferred execution (via the @defer directive) allows a GraphQL query to return a response incrementally — sending the primary data first and deferring slower fields to stream later. Example: query { user(id: "1") { name ... @defer { slowExpensiveField { data } } } } — the user's name arrives immediately; the deferred section arrives later via a multipart HTTP stream. The transport uses multipart/mixed content type (similar to HTTP streaming). Benefits: faster initial render, better user experience for queries with mixed fast/slow fields, allows progressive rendering. Requires server support (Apollo Server 4+ with @defer plugin, Yoga) and client support (Apollo Client 3.7+). The @stream directive is related — it streams list items as they are resolved rather than waiting for the full list.
24
How do you implement field-level caching with cache hints in GraphQL?
GraphQL doesn't have native HTTP caching since all queries are POST requests to a single endpoint. Apollo Server's cache control plugin allows setting cache hints per field that propagate to the overall response. In the schema: type Query { publicPosts: [Post!]! @cacheControl(maxAge: 60) } type Post @cacheControl(maxAge: 300) { id title }. Or programmatically in resolvers: info.cacheControl.setCacheHint({ maxAge: 60, scope: 'PUBLIC' }). The response includes Cache-Control: max-age=60, public headers (for GET persisted queries). CDNs like Cloudflare or Fastly can then cache responses. scope: PRIVATE prevents CDN caching for user-specific data. The minimum maxAge across all fields in the query is used. This works best with persisted queries (GET requests) since POST requests aren't normally cached by CDNs.
Deep expertise questions for senior and lead roles.
01
What is Apollo Federation v2 and how does the supergraph work?
Apollo Federation v2 is the industry standard for composing multiple GraphQL services (subgraphs) into a single unified API (supergraph). Architecture: each team owns a subgraph with its own schema and resolvers; the Apollo Router (Rust-based gateway) receives client queries, runs the query planner to determine which subgraphs to call and in what order, executes the plan (potentially fetching from multiple subgraphs in parallel), and merges results. Key directives: @key(fields: "id") (entity primary key — enables cross-service entity resolution), @shareable (field can be defined in multiple subgraphs), @override(from: "SubgraphName") (migrate field ownership between subgraphs), @provides/@requires (optimize resolver execution by pre-providing fields). Schemas are composed with the Rover CLI. Apollo GraphOS manages schema checks, schema registry, and usage analytics.
02
How do you implement entity resolution in Apollo Federation?
Entity resolution is how Federation resolves entities (types with @key) across subgraph boundaries. The process: (1) Service A returns a stub reference: { __typename: "User", id: "42" }. (2) The Router sends these stubs to Service B's _entities query: query(_representations: [{ __typename: "User", id: "42" }]) { _entities { ... on User { orders { id } } } }. (3) Service B's __resolveReference function fetches the full entity: User: { __resolveReference: (ref) => db.user.findById(ref.id) }. Complex keys: @key(fields: "region { name }") — nested key fields. Multiple keys: a type can have multiple @key directives. Stubbed entities: Service B doesn't need to implement all User fields — only those it extends. The Router's query planner optimizes entity fetching with batching.
03
What are the security vulnerabilities specific to GraphQL?
GraphQL introduces unique security risks: (1) Introspection abuse: attackers enumerate the full schema. Mitigation: disable introspection in production, or use allow-listing. (2) Denial of Service via complex queries: deeply nested or expensive queries overwhelm the server. Mitigation: query depth limiting, complexity analysis, timeout, query size limit. (3) Batching attacks: send many operations in one request. Mitigation: limit operation count per request. (4) Field suggestion exploitation: error messages suggest similar field names, aiding schema discovery. Mitigation: disable field suggestions in production. (5) SQL/NoSQL injection: via args passed unsanitized to databases. Mitigation: use parameterized queries/ORM. (6) Authorization bypass: accessing fields or data without proper resolver-level auth checks. Mitigation: check auth in every resolver or use shield/directives. (7) Mass assignment: input types accepting too many fields. Mitigation: validate and whitelist input fields. Tools: graphql-armor adds multiple protections in one package.
04
How does the GraphQL query planning and execution pipeline work?
A GraphQL request goes through five phases: (1) Parsing: the query string is lexed and parsed into an AST (Abstract Syntax Tree) by graphql-js. Syntax errors are caught here. (2) Validation: the AST is validated against the schema — unknown fields, type mismatches, missing required arguments, and circular fragments are caught. Custom validation rules (depth limits, complexity) run here. (3) Execution planning (Federation only): the Router's query planner analyzes the query, identifies which subgraphs own which fields, and creates a parallel/sequential execution plan. (4) Execution: resolvers are called. Query field resolvers run concurrently (Promises collected, then awaited). Mutation top-level fields run serially. (5) Response formatting: results are merged, null propagation applied for non-null errors, and the response is serialized to JSON. The errors array is populated from any resolver errors. Each phase is a potential plugin hook in Apollo Server.
05
What is the @defer and @stream directive and how do they work?
@defer and @stream are incremental delivery directives that allow GraphQL to send partial results as they become available, using HTTP multipart responses. @defer defers a fragment: query { user { name ... @defer(label: "profile") { bio posts { title } } } }. The server sends the initial response (with name, placeholder for deferred), then sends deferred chunks incrementally via multipart/mixed chunks. Each chunk has a hasNext flag. @stream streams list items: query { users @stream(initialCount: 3) { name } } — sends the first 3 immediately, streams the rest. Transport: uses multipart HTTP for HTTP/1.1 and can leverage HTTP/2 server push. The client (Apollo Client 3.7+) handles merging incremental results into the cache. Server support: Apollo Server 4 with @apollo/server and the ApolloServerPluginSchemaReporting pattern, GraphQL Yoga. Extremely valuable for dashboards and slow-loading pages.
06
How do you design a scalable GraphQL schema?
Scalable schema design principles: (1) Follow the Relay spec: global IDs (Node interface), cursor pagination (Connection/Edge), single input argument mutations. (2) Verb-noun mutation naming: createPost, updateUser, deleteComment — clear and consistent. (3) Rich payloads: mutations return payload objects, not just the modified entity — allows adding fields (e.g., errors, userErrors) without breaking changes. (4) Nullability by design: be conservative with non-null — easy to make nullable fields non-null later (non-breaking), hard to reverse. (5) Abstract types: use interfaces for shared structures, unions for heterogeneous results. (6) Avoid over-normalization: don't model every database relation as a GraphQL type — model the client's needs. (7) Never expose internal IDs: use opaque global IDs. (8) Version by field deprecation: add new fields, deprecate old ones — no versioning needed. (9) Think about mutations atomically: each mutation should be a single business operation.
07
What are the best practices for error handling in GraphQL APIs?
Production GraphQL error handling best practices: (1) Typed user errors: use the "errors-as-data" pattern for expected business errors — return them in the payload rather than the top-level errors array: type CreateUserPayload { user: User errors: [UserError!] }. Reserve top-level errors for unexpected/system errors. (2) Error codes: always include machine-readable codes in extensions.code: UNAUTHENTICATED, FORBIDDEN, NOT_FOUND, VALIDATION_ERROR. (3) Mask internal errors: never expose stack traces, SQL queries, or internal messages to clients — log them server-side, return generic messages. (4) Logging: log all resolver errors with context (user ID, operation name, variables). (5) Error boundaries: use null propagation behavior intentionally — design nullable vs non-null fields to control error impact scope. (6) Client error handling: always check both data and errors — Apollo's errorPolicy: 'all' returns partial data alongside errors.
08
What is the GraphQL Mesh library?
GraphQL Mesh (by The Guild) is a tool that generates a unified GraphQL API over any data source — REST APIs, gRPC services, SOAP, GraphQL, database ORM, OpenAPI specs, and more — without writing resolvers manually. It reads the source schema/spec (OpenAPI YAML, gRPC proto, GraphQL SDL), generates a GraphQL layer on top, and serves it as a single endpoint. Key features: (1) Transforms: rename types, filter fields, add resolvers, change nullability. (2) Merging: combine multiple sources into one schema (like stitching but declarative). (3) Type safety: generates TypeScript types for all sources. (4) Plugins: caching, mocking, rate limiting, auth. (5) Federation compatibility: can produce Federation-compatible subgraphs. Configuration is YAML-based: .meshrc.yaml. Excellent for wrapping legacy REST APIs in GraphQL without extensive development, or building a unified API layer over a microservice landscape.
09
How do you implement schema evolution and versioning in GraphQL?
GraphQL's approach to evolution avoids explicit versioning through continuous schema evolution: (1) Add, don't remove: always add new fields/types; never remove them immediately. Old clients continue working. (2) Deprecation: mark old fields with @deprecated(reason: "Use newField instead") — tools warn developers; field still works. (3) Usage analytics: use Apollo Studio's field usage tracking to identify when deprecated fields have zero usage before removing them. (4) Backward-compatible changes: safe — adding optional arguments (with defaults), adding nullable fields, adding types, adding enum values (carefully — clients should handle unknown values). (5) Breaking changes: unsafe — removing fields, changing non-null to null (ok) or null to non-null (breaking), changing argument types, renaming. (6) Schema checks: use rover graph check or Apollo Studio to automatically detect breaking changes in CI. (7) Field aliasing: add new field returning differently shaped data; alias old field to new implementation during transition.
10
What is the GraphQL Codegen tool and how is it used?
GraphQL Code Generator (graphql-codegen by The Guild) automatically generates TypeScript types, React hooks, resolvers, and more from your GraphQL schema and operations. Setup: npm install -D @graphql-codegen/cli @graphql-codegen/typescript. Configure codegen.yml: schema: http://localhost:4000/graphql documents: src/**/*.graphql generates: src/generated/graphql.ts: plugins: [typescript, typescript-operations, typescript-react-apollo]. Running graphql-codegen generates: TypeScript interfaces for all schema types, typed operation hooks (useGetUsersQuery), typed resolvers for the server. Benefits: (1) End-to-end type safety: client operations match server schema at compile time. (2) Auto-complete: IDE knows the shape of query results. (3) Catch schema drift: codegen fails if operations reference deleted fields. (4) Reduced boilerplate: no manual type writing. Run in watch mode during development: graphql-codegen --watch.
11
What is GraphQL subscriptions with WebSocket protocol graphql-ws vs subscriptions-transport-ws?
Two WebSocket protocols for GraphQL subscriptions: subscriptions-transport-ws: the original library (by Apollo), using the graphql-ws (confusingly different from the library below) sub-protocol string. It is now deprecated and unmaintained. Known issues: connection problems, unclear error handling, protocol design issues. graphql-ws library: the modern, actively maintained replacement by Denis Badurina. Uses the graphql-transport-ws sub-protocol. Key improvements: clear connection lifecycle (Connected → Ping/Pong → Subscribe → Next/Error/Complete), proper error handling, lazy connection mode, reconnection support, and active maintenance. Apollo Client 3.5+ supports graphql-ws: const wsLink = new GraphQLWsLink(createClient({ url: 'ws://localhost:4000/subscriptions' }));. Server setup: useServer({ schema }, wsServer);. Always use graphql-ws for new projects. Migrate existing projects away from subscriptions-transport-ws.
12
How do you implement distributed tracing for GraphQL?
Distributed tracing in GraphQL helps identify performance bottlenecks at the field level. Approaches: (1) Apollo Tracing: Apollo Server's built-in tracing adds resolver timing to the response's extensions object — enable with the ApolloServerPluginInlineTrace plugin. Apollo Studio automatically collects and displays these traces. (2) OpenTelemetry: use @opentelemetry/api with a GraphQL instrumentation plugin (@opentelemetry/instrumentation-graphql) — creates spans for parse, validate, execute, and individual resolver calls. Send traces to Jaeger, Zipkin, Tempo, or Honeycomb. (3) Custom resolver middleware: wrap all resolvers with timing logic and send to your APM. (4) Logging: log slow resolvers (>100ms) with their full path and arguments. Key metrics to track: per-operation latency percentiles (p50, p95, p99), per-field resolver latency, cache hit rates, error rates by operation. Set up alerts for p99 latency regressions and error rate spikes.
13
What are the trade-offs of using GraphQL vs REST for different use cases?
GraphQL excels in: (1) Complex data requirements: nested, relational data from multiple resources in one request. (2) Multiple clients: mobile, web, partner APIs each request exactly what they need. (3) Rapid iteration: frontend evolves independently — no need to deploy backend API changes for new field combinations. (4) Type safety + codegen: end-to-end typed clients. REST excels in: (1) Simple CRUD APIs: low overhead, HTTP caching works naturally. (2) File uploads/downloads: REST is more natural. (3) Public APIs: REST is more widely understood, better HTTP cache support, standard tools (Swagger/OpenAPI). (4) Event streaming: SSE, chunked transfer — REST is simpler. (5) CDN caching: REST GET requests are cacheable at the HTTP layer without extra tooling. Trade-offs: GraphQL requires more server infrastructure (DataLoaders, complexity limits), has a steeper learning curve, and HTTP caching is harder. For internal APIs with complex frontends, GraphQL often wins. For simple public APIs, REST is often simpler.
14
How do you test GraphQL APIs effectively?
Comprehensive GraphQL testing strategy: (1) Unit test resolvers: test resolver functions directly with mock context and args — no HTTP needed. expect(await userResolver({}, { id: '1' }, { db: mockDb })).toMatchObject({ name: 'Alice' });. (2) Integration test with a real schema: use graphql() function directly: const result = await graphql({ schema, source: GET_USER, variableValues: { id: '1' } });. (3) End-to-end tests: use supertest or a test HTTP client against a real running server with a test database. (4) Schema validation tests: test that schema loads without errors, required fields are non-null, deprecated fields have reasons. (5) Mock a GraphQL server: use graphql-tools addMocksToSchema for frontend testing without a real backend. (6) Contract testing: use Apollo Studio's schema checks in CI. (7) DataLoader testing: mock the batch function and verify batching behavior. Tools: Jest (test runner), graphql package (execution), @graphql-tools/mock, Apollo Server's testing utilities.
15
What is operation complexity and how does it relate to rate limiting GraphQL APIs?
Standard IP/request-based rate limiting is insufficient for GraphQL — a single request can be vastly more expensive than another. Effective rate limiting uses operation complexity as the currency. Architecture: (1) Calculate complexity: use graphql-query-complexity to compute cost per operation at validation time. Reject if > max threshold. (2) Complexity budget: assign each user/API key a rolling complexity budget (e.g., 10,000 per minute). Deduct per operation. Track budgets in Redis with sliding window counters. (3) Field-level cost: expensive fields (external API calls, ML inference) get higher costs than simple DB lookups. (4) Response time budget: implement server-side timeouts — setTimeout(() => query.cancel(), 30000). (5) Request size limit: reject queries over N bytes (prevents large payload attacks). (6) Apollo Router rate limiting: Apollo's router supports JWT-based rate limiting per user/operation. (7) Persisted queries only: only allow pre-registered operations — eliminates arbitrary query attacks entirely.