← IT Knowledge Base GraphQL · all questions

GraphQL — all questions in one page

1011 questions and answers on GraphQL, from the schema and the type system to federation, cost limits and production hardening. Numbered continuously and searchable: fundamentals, schema design and pagination, execution and resolvers, caching and N+1, security, subscriptions, tooling across HotChocolate, Apollo, Yoga and gqlgen, plus copy-usable recipes. Search jumps straight to a question; the buttons filter by level or show the recipes.

1011 questions Beginner · 280 Intermediate · 340 Advanced · 377 Recipes · 14 English GraphQL spec · 2021+

Beginner 280 questions

Fundamentals: schema and SDL, types and scalars, queries, mutations, subscriptions, variables, fragments, directives, introspection and the HTTP transport.

1. What is GraphQL and what problem does it solve?

GraphQL is a query language and runtime for APIs that lets clients ask for exactly the data they need in a single request. It solves the problem of clients having to call many fixed endpoints and stitch results together. The server exposes one typed schema and a single endpoint, and the client describes the response shape in the query itself. This gives the client control over the payload while keeping the server's data model strongly typed.

2. What are the main differences between GraphQL and REST?

REST exposes many resource-oriented endpoints with fixed response shapes, while GraphQL exposes one endpoint with a typed schema and client-defined selections. REST relies on HTTP verbs and status codes for semantics; GraphQL typically uses POST with an operation document and returns a data plus errors envelope. GraphQL avoids multiple round trips for related data but adds query complexity, caching challenges, and the need for depth and cost limits.

3. What is over-fetching in REST, and how does GraphQL reduce it?

Over-fetching happens when an endpoint returns more fields than the client needs, wasting bandwidth and parsing time. GraphQL lets the client list only the fields it will actually use, so unused attributes are never resolved or transferred. This is especially valuable on mobile or slow networks. Note that over-fetching can still occur if the client writes sloppy selections.

query { user(id: "1") { name } }

4. What is under-fetching, and how does GraphQL address it?

Under-fetching means one endpoint does not return enough data, forcing the client to make follow-up requests. GraphQL solves this by allowing nested selections that traverse relationships in one operation. A single query can fetch a user, their posts, and each post's comments. That reduces chattiness and client-side coordination logic.

5. Why is the schema described as a contract?

The schema declares every type, field, argument, and nullability rule the API supports, so clients and servers can evolve independently against a shared agreement. Clients can rely on the schema to know what is valid, and tooling can validate queries before execution. Breaking changes become visible as schema changes rather than runtime surprises. In practice the schema is the source of truth for both documentation and code generation.

6. What is SDL and what does it look like?

SDL, the Schema Definition Language, is the human-readable syntax for declaring GraphQL types without executable logic. It uses type, input, enum, interface, and union keywords with field lists in braces. Most servers can print their schema as SDL and most tools consume SDL files directly.

type User {
  id: ID!
  name: String!
  email: String
}

7. What are the three root operation types in GraphQL?

They are Query for reads, Mutation for writes, and Subscription for long-lived event streams. A schema must define a Query type, while Mutation and Subscription are optional. Each root type is just an ordinary object type whose fields act as entry points. Clients select fields from these roots at the top level of an operation.

8. How do you declare a custom root operation type name?

You add a schema definition that maps each operation to a concrete type name. This is useful when your root types follow a naming convention other than Query, Mutation, and Subscription. Without an explicit schema block, GraphQL assumes the default names.

schema {
  query: RootQuery
  mutation: RootMutation
}

9. What is an object type in GraphQL?

An object type is a named collection of fields, each with its own type, that represents a domain entity such as a User or Product. Object types can reference each other, which is how the graph is formed. Fields may take arguments and may return scalars, enums, objects, interfaces, or unions. Every field resolution is independent, so each one may hit a different data source.

10. What are the built-in scalar types in GraphQL?

The specification defines Int, Float, String, Boolean, and ID. Int is a signed 32-bit integer, and Float is a double-precision value. ID serializes like a string but semantically identifies an object. Servers can add custom scalars for anything else.

11. When would you introduce a custom scalar?

You introduce a custom scalar when no built-in type accurately represents the value, such as DateTime, URL, JSON, or a money amount with fixed precision. A custom scalar must implement serialization, parsing of literal values, and parsing of variable input. The server should reject invalid values so the contract stays meaningful. Clients and codegen then treat the field as a distinct type.

scalar DateTime

12. What is the difference between a scalar and an enum?

A scalar represents a single primitive value such as a string or number, while an enum restricts a value to one of a fixed set of named options. Enums are validated at query time, so a typo becomes an error instead of bad data. They also self-document allowed values in the schema.

enum Role {
  ADMIN
  EDITOR
  VIEWER
}

13. How do you declare an enum and use it in a query?

Enums use the enum keyword with a list of uppercase names by convention. They can appear as field types, argument types, or input fields. In queries, enum values are written as bare names without quotes. A quoted string would be rejected as a type mismatch.

query {
  users(role: ADMIN) { id }
}

14. What does the list modifier [...] mean in a type?

Square brackets indicate a list, meaning an ordered collection of the wrapped type. The list itself may be nullable or non-null, and its elements may independently be nullable or non-null. [String!]! is a required list of required strings, while [String] may be null and may contain nulls. These distinctions drive client null handling.

15. What does the non-null modifier ! guarantee?

A trailing exclamation marks a field, argument, or input value as non-null, so the server promises never to return null there. If a resolver for a non-null field does return null, the error propagates upward, potentially nulling the parent object. Marking too much as non-null therefore makes partial failures harsher. Use it deliberately for values that are truly always present.

16. What is an interface in GraphQL?

An interface defines a shared set of fields that implementing object types must include. Querying an interface returns any implementing type, so clients use fragments or inline fragments to select type-specific fields. Interfaces express "is a" relationships in the schema. They are resolved at runtime with a type resolver.

interface Node { id: ID! }
type User implements Node { id: ID! name: String! }

17. What is a union type and how does it differ from an interface?

A union is a set of object types with no required shared fields; it only says the value is one of the members. An interface, by contrast, enforces common fields across implementers. Because unions have no fields of their own, clients must always use inline fragments or fragments with type conditions. Unions are handy for search results or heterogeneous feeds.

union SearchResult = User | Post | Comment

18. How do you query a union or interface field?

You query common fields directly, then use inline fragments with ... on TypeName to access the fields of specific members. The client should include a discriminator such as __typename so it can tell which type came back. Leaving out type-specific fragments means those fields are simply not returned.

query {
  search(term: "api") {
    __typename
    ... on User { name }
    ... on Post { title }
  }
}

19. What is an input type and when do you need one?

Input types use the input keyword and describe structured argument values sent by the client. They are required because object types cannot be used as argument types in GraphQL. Use them when a mutation or query takes several related arguments, such as a filter or a create payload. Input fields may be scalars, enums, lists, or other input types.

input CreateUserInput {
  name: String!
  email: String!
  role: Role = VIEWER
}

20. Why can't an object type be used as an argument type?

The specification separates input and output types because they have different capabilities and semantics. Output types can have arguments, interfaces, and resolver behavior, while input values must be inert serializable data. Allowing output types as arguments would create ambiguity about cycles and validation. That is why the schema defines distinct input shapes.

21. How do field arguments work in GraphQL?

Arguments are declared in parentheses after a field name with a name and a type, and clients supply values at query time. They let a single field serve many variations, such as filtering, pagination, or locale. Arguments may be literals or variables, and they are validated against the schema. Resolvers receive them in the second positional parameter.

type Query {
  posts(first: Int = 10, tag: String): [Post!]!
}

22. How do default argument values work?

A default is written after the argument type and is used when the client omits the argument entirely. Passing explicit null is different from omitting the argument when the argument is nullable. Defaults are part of the schema and are visible through introspection. They are a convenient way to keep queries short while preserving sensible behavior.

type Query {
  posts(limit: Int = 20): [Post!]!
}

23. What are field names and what restrictions apply?

Field names must match the GraphQL name grammar, which allows letters, digits, and underscores but cannot start with a digit. Names beginning with two underscores are reserved for introspection. Field names are case-sensitive, so userName and username are different fields. Consistent naming conventions make a schema far easier to consume.

24. What is an alias and why use one?

An alias renames a field in the response without changing the schema. Aliases let you request the same field twice with different arguments in one selection set. They also let clients match their own naming conventions or avoid collisions when merging fragments.

query {
  first: user(id: "1") { name }
  second: user(id: "2") { name }
}

25. What happens if you select the same field twice without aliases?

The fields are merged by response key, and GraphQL requires them to be compatible. If both selections have the same arguments and subfields, the result is a single field. If arguments conflict, validation fails with an error about conflicting fields. Aliases are the standard fix when you need genuinely different arguments.

26. What is a named fragment and what problem does it solve?

A named fragment is a reusable selection set defined with fragment Name on Type and spread with .... It avoids repeating identical field lists across queries and components. Fragments are colocated with UI code in many client frameworks. The fragment's type condition must be compatible with where it is spread.

fragment UserFields on User { id name }
query { me { ...UserFields } }

27. What is an inline fragment used for?

An inline fragment applies a selection set to a specific type or condition without declaring a reusable name. It is the standard way to read fields on union members and interface implementations. It can also add a directive-driven condition. Inline fragments never appear in the response as a key.

... on Admin { permissions }

28. Why should queries use variables instead of literal values?

Variables separate the query document from the input values, so the same document can be reused with different arguments. That enables server-side caching of parsed and validated documents and safer handling of user input. Variables are also strongly typed and validated against the declared variable types. String interpolation into a query document is both fragile and a security risk.

29. How do you declare and pass variables in a query?

Declare the variable with a $ prefix and a type after the operation name, then use it in the selection. Values are supplied separately in a JSON variables object. The declared type must match the argument type, with non-null rules respected.

query GetUser($id: ID!) {
  user(id: $id) { name }
}
// variables: { "id": "1" }

30. What are the @skip and @include directives?

Both are built-in directives that take a required if argument of type Boolean!. @skip(if: true) omits the field or fragment, and @include(if: true) keeps it. They let one query adapt its shape at runtime based on variables. The two can be combined, and the field is excluded if either condition excludes it.

query ($withEmail: Boolean!) {
  user(id: "1") { name email @include(if: $withEmail) }
}

31. What is the difference between @skip with true and passing null?

@skip(if: true) removes the field from the response entirely, so the key is absent. A null value in the response means the field was requested and resolved to null, which is a data fact. Clients must distinguish "not requested" from "requested but empty". You cannot pass null to if because it is non-null.

32. What is a mutation and how is it different from a query?

A mutation is an operation that changes server-side state and is declared against the Mutation root type. Top-level mutation fields are executed serially in the order given, whereas query fields may be resolved in parallel. Mutations return a value, usually the created or updated object, so the client can update its cache. They should still be treated as non-idempotent unless designed otherwise.

mutation {
  createPost(input: { title: "Hi" }) { id title }
}

33. What is a subscription and how does it work?

A subscription lets a client receive a stream of events over a long-lived connection, usually WebSocket or server-sent events. The client sends a subscription operation and the server pushes a new payload each time the event fires. Each payload has the same shape as the subscription selection. Subscriptions are for real-time updates, not for request-response flows.

subscription {
  postAdded { id title }
}

34. What is introspection and what is __schema?

Introspection is the ability to query the schema itself using reserved fields such as __schema, __type, and __typename. Tools like GraphiQL use it to build documentation, autocomplete, and validation. Introspection can be disabled in production to reduce information exposure, though that does not replace authorization. It is also what powers schema diffing and client codegen.

query { __schema { queryType { name } } }

35. What is a description in SDL and how is it written?

A description is documentation attached to a type, field, argument, enum value, or directive using a triple-quoted string. It appears in introspection and therefore in GraphiQL, Apollo Sandbox, and generated docs. Descriptions support markdown in most tooling. They are part of the schema contract, not throwaway comments.

"""A registered account holder."""
type User {
  """Unique, stable identifier."""
  id: ID!
}

36. How do comments differ from descriptions in GraphQL?

Comments start with #, are ignored by the parser, and never appear in introspection. Descriptions are string literals attached to schema elements and are visible to tooling and clients. Use comments for notes to maintainers and descriptions for API consumers. A comment cannot document a field for the client.

37. How do you deprecate a field with @deprecated?

Apply @deprecated to a field definition, optionally with a reason string, and introspection marks it as deprecated along with that reason. Editors strike the field through and warn on use, but queries still execute. Deprecation is the recommended alternative to versioning. It can also be applied to enum values and arguments in newer specifications.

type User {
  name: String! @deprecated(reason: "Use fullName instead.")
  fullName: String!
}

38. What is the GraphQL endpoint and how does HTTP transport work?

The endpoint is a single URL that accepts every operation for the schema, commonly /graphql. The client sends a POST request whose JSON body includes query and optionally variables and operationName. The server responds with a JSON document containing data and, when relevant, errors. There is no per-resource routing.

{ "query": "{ me { id } }", "variables": {} }

39. When is it acceptable to send a GraphQL query with GET?

GET is acceptable for read-only query operations, passing the document and variables as query parameters such as ?query=.... It enables HTTP caching, CDN caching, and shareable URLs. Mutations must not use GET because intermediaries may prefetch or replay them. Long documents can exceed URL length limits, so POST remains the safer default.

40. Which content types are used for GraphQL over HTTP?

application/json is the standard and most widely supported, with the operation in the JSON body. application/graphql sends the raw document as the body but cannot carry variables, so it is rarely used. Some servers also accept application/x-www-form-urlencoded for simple GET-style posts. Clients should always send a correct Content-Type header to avoid 415 responses.

41. Why should every production operation have a name?

Operation names appear in server logs, traces, and error reports, making it possible to identify which client query caused a problem. Tooling can also generate per-operation metrics and persisted query identifiers. Anonymous operations still work but produce opaque telemetry. Naming is a convention enforced by linting rather than by the specification.

42. How do you include multiple operations in one document?

You define several named operations in the same document and specify which one to run with operationName. The server executes only the selected operation. Multiple anonymous operations are invalid because they cannot be disambiguated. This pattern is common in tooling and generated client artifacts.

{ "query": "query A { me { id } } query B { __typename }", "operationName": "A" }

43. What happens if you send multiple operations without operationName?

The request fails validation because the server cannot decide which operation to execute. The error is returned in the errors array, and data is absent or null. If the document contains exactly one operation, operationName may be omitted. Many clients always send it for clarity.

44. What is the required JSON shape for variables?

Variables must be a JSON object whose keys are the variable names without the $ prefix. Values must match the declared types, including list and input object structure. A missing required variable or a type mismatch is a request error before execution. Nested input objects follow the schema's input field names exactly.

{ "id": "1", "filter": { "tag": "api", "limit": 5 } }

45. What are common nullability pitfalls when designing a schema?

Overusing ! makes partial failures catastrophic because one null bubbles up and nulls the parent. Underusing it forces every client to handle nulls that never occur. Changing a nullable field to non-null is a breaking change for existing queries. Design from real invariants and prefer nullable for values that may legitimately be unknown.

46. How does null propagation work when a non-null field fails?

If a resolver for a non-null field returns null, the error is recorded and propagates to the nearest nullable ancestor. That ancestor becomes null while sibling fields may still resolve. If the root itself is non-null, the whole data value becomes null. This is why granular nullable fields give better partial results.

47. Why are nested selections central to thinking in GraphQL?

GraphQL models data as a graph, so a query follows relationships from a root field into related objects. Nested selections replace the multiple REST calls a client would otherwise make. Thinking in graphs means asking "what shape does the UI need" rather than "which endpoint do I call". It also helps identify expensive paths that need depth or cost limits.

query {
  user(id: "1") {
    name
    posts(first: 3) { title comments { body } }
  }
}

48. What is the difference between schema-first and code-first development?

Schema-first means writing SDL by hand and then wiring resolvers to it, often with generated types. Code-first means defining types in the host language and generating the SDL from that code. Schema-first favors design review and cross-team contracts, while code-first reduces duplication and keeps types in sync. Either way the published schema should be treated as the contract.

49. What tools can you use to explore a GraphQL API?

GraphiQL is a lightweight in-browser IDE with docs and autocomplete, and Apollo Sandbox is a hosted explorer with similar features plus tracing. Altair adds environments, subscriptions, and header management. All of them rely on introspection to render schema documentation. Most servers can expose one of them in development only.

50. How do you set up a minimal Apollo Server?

Install the server package, define your typeDefs and resolvers, create the instance, then start it and mount it on an HTTP server. Apollo Server 4 exposes a startStandaloneServer helper for quick setups. Resolvers map each schema field to a function returning its value.

const server = new ApolloServer({ typeDefs, resolvers });
const { url } = await startStandaloneServer(server, { listen: { port: 4000 } });

51. How do you run a basic query with Apollo Client?

Create a client with an HTTP link pointing at your endpoint and an in-memory cache, wrap the app in a provider, then call useQuery with a document and variables. The hook returns loading, error, and data. Apollo caches normalized results by __typename and id. Mutations use useMutation and can update the cache afterward.

const { data, loading, error } = useQuery(GET_USER, { variables: { id: "1" } });

52. How do you call a GraphQL API with plain fetch?

Send a POST with a JSON content type and a body containing query and variables, then parse the JSON response. Remember that a 200 status may still contain errors, so check the errors array. Plain fetch avoids client library weight but leaves caching and normalization to you.

const res = await fetch("/graphql", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ query, variables })
});
const { data, errors } = await res.json();

53. Which HTTP status codes does a GraphQL server typically return?

A successfully parsed and executed request returns 200 even when field errors occurred. Malformed JSON or a missing query typically yields 400, and authentication failures often yield 401 or 403. Server crashes may return 500. Some servers use 200 for all well-formed transport requests and rely on the body for GraphQL errors.

54. What does the GraphQL error response look like?

Errors appear in an errors array, each with a message and optionally locations, path, and extensions. The path points to the response key where the failure occurred, which helps map errors to fields. extensions carries machine-readable codes such as UNAUTHENTICATED. Partial results may still be present in data.

{ "data": { "user": null },
  "errors": [{ "message": "Not found", "path": ["user"], "extensions": { "code": "NOT_FOUND" } }] }

55. Why does GraphQL often return HTTP 200 when there are errors?

Field errors are application-level results, not transport failures: the request was delivered and the document executed, possibly returning partial data. The specification keeps transport status separate from execution errors so clients can consume partial responses. Network or parsing problems still deserve 4xx or 5xx statuses. Clients must inspect the body rather than trusting the status alone.

56. Can you batch multiple GraphQL queries in a single HTTP request?

Yes, commonly by combining fields under one operation with aliases, or by sending a JSON array of operations to servers that support query batching. Alias-based batching is fully spec-compliant and works everywhere. Array batching is a non-standard transport feature and must be enabled explicitly. Batching reduces round trips but complicates caching and error mapping.

query {
  a: user(id: "1") { name }
  b: user(id: "2") { name }
}

57. What naming conventions are common in GraphQL schemas?

Types use PascalCase, fields and arguments use camelCase, and enum values use SCREAMING_SNAKE_CASE. Query fields are usually nouns and mutations are verb phrases such as createPost or updateUser. Boolean fields read as predicates like isPublished. Consistency matters more than any single convention because generated clients inherit these names.

58. What makes for good schema documentation?

Write descriptions on every public type, field, argument, and enum value, explaining semantics rather than restating the name. Document units, formats, nullability meaning, pagination behavior, and authorization requirements. Include deprecation reasons and migration guidance. Keep descriptions in the SDL so they ship with the contract and appear in every explorer.

59. What are common beginner mistakes in GraphQL?

Typical mistakes include over-marking fields non-null, building one giant query instead of using fragments, interpolating user input into documents instead of using variables, and ignoring partial errors. Others are modeling endpoints instead of domain types, forgetting input types for mutations, and assuming HTTP status codes signal GraphQL failures. Most of these show up as brittle clients or noisy production errors.

60. What is __typename and why is it useful?

__typename is a reserved meta-field that returns the concrete type name of the current object. It is essential for distinguishing union members and interface implementations on the client. Normalized caches also use it, together with id, as the cache key. It can be requested in any selection set without being declared in the schema.

query { node(id: "1") { __typename id } }

61. What is the difference between a null list and an empty list?

An empty list means the collection exists but has no members, while null means the value is absent or unavailable. Clients often render these differently, so the schema should make the intent clear. For required collections, prefer returning an empty list rather than null. That avoids null checks scattered through UI code.

62. How does cursor-based pagination typically look in a schema?

Edges carry a cursor plus the node, and pageInfo exposes hasNextPage, hasPreviousPage, startCursor, and endCursor. The client passes first and after to move forward. Cursors are opaque strings that stay stable when items are inserted. This is the Relay connection pattern and is widely adopted even outside Relay.

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

63. What are the trade-offs of offset pagination versus cursor pagination?

Offset pagination with limit and offset is simple and allows jumping to a page, but it drifts when rows are inserted or deleted and gets slow on large offsets. Cursor pagination is stable and efficient for infinite scrolling but cannot jump arbitrarily. Many APIs expose both, using cursors for feeds and offsets for admin tables.

64. What arguments does a GraphQL resolver receive?

A resolver receives the parent value, the field arguments, a shared context object, and an info object describing the field and operation. The parent is the result of the enclosing resolver. Context carries per-request state such as the authenticated user and data loaders. Info exposes the AST and schema details for advanced use cases.

const resolvers = {
  Query: { user: (_, args, ctx) => ctx.db.user.findById(args.id) }
};

65. What belongs in the GraphQL context object?

Context holds request-scoped dependencies such as the authenticated user, authorization helpers, database clients, and DataLoader instances. It is created once per request and passed to every resolver, so it is the right place for shared state. Never store mutation-prone global state there. Keep it small and typed for safety.

66. What is the N+1 problem in GraphQL?

N+1 happens when resolving a list of N parents triggers one extra query per parent for a nested field. A query for twenty posts and their authors can produce twenty-one database calls. GraphQL makes this easy to hit because clients choose the nesting. Batching tools such as DataLoader collapse the per-parent lookups into a single batched query.

67. How does DataLoader help with batching and caching?

DataLoader collects all keys requested within a single tick, issues one batched load function call, and distributes results back to each caller. It also caches results per request, so repeated keys resolve once. Create a new loader per request to avoid leaking data between users. It solves batching, not authorization or query cost.

const userLoader = new DataLoader(ids => db.users.findByIds(ids));

68. What are persisted queries and what problem do they solve?

Persisted queries store the operation text on the server or a CDN and let clients send only a hash or ID. This shrinks request size and prevents arbitrary query documents from reaching the server. It also acts as an allowlist that blocks unexpected operations. Teams often combine it with automatic persisted queries for gradual rollout.

69. Why do production GraphQL servers limit query depth and cost?

Because clients control selection shape, a single deeply nested or widely aliased query can exhaust CPU and database capacity. Depth limits cap nesting, and cost analysis assigns weights to fields and multipliers for lists. Servers then reject operations above a threshold before execution. Rate limiting per operation name or IP complements these limits.

70. How do you evolve a GraphQL schema without versioning?

Prefer additive changes: add fields, arguments with defaults, enum values, and new types. Deprecate rather than remove, and monitor field usage before deleting anything. Avoid changing nullability in stricter directions or renaming fields, since both break existing documents. Schema registries can detect breaking changes during review.

71. Where should authorization be enforced in a GraphQL server?

Enforce it in resolvers or a business layer that every resolver calls, never only in the client or at the gateway. Field-level checks matter because a single query can traverse many resources. Return a consistent error code such as FORBIDDEN and avoid leaking whether an object exists. Context is the natural place to carry the identity used for those checks.

72. What is a custom directive and when would you write one?

A custom directive declares reusable behavior attached to schema elements or operations, using directive @name on LOCATION. Type-system directives can drive codegen, validation, or access rules, while executable directives transform execution. Most teams only need built-ins like @deprecated and @skip. Reach for a custom directive when you need the same annotation repeated across the schema.

directive @auth(role: Role!) on FIELD_DEFINITION

73. What is the difference between executable and type-system directives?

Executable directives such as @skip, @include, and @defer appear in client operations and affect execution. Type-system directives appear in SDL on types and fields to describe metadata or tooling behavior. The locations list in the directive definition determines where each may appear. A single directive can be declared for both kinds of locations if needed.

74. What does a repeatable directive allow?

By default a directive may appear at most once per location. Marking it repeatable allows multiple applications at the same location, such as several @tag annotations on one field. Without the keyword, duplicates are a validation error. This is useful for schema metadata and federation tooling.

directive @tag(name: String!) repeatable on FIELD_DEFINITION

75. How do you define a self-referencing type, such as a tree node?

A type may reference itself in its own fields, which models recursive structures like categories or comment threads. GraphQL places no limit on the reference itself, but clients must use fragments to select arbitrarily deep levels. Servers should still enforce depth limits at execution time. Nullable self-references are typical for leaves.

type Category {
  id: ID!
  name: String!
  children: [Category!]!
}

76. Does field order in the response follow the query?

Yes, GraphQL specifies that object fields in the response appear in the same order as they were selected. This makes responses deterministic and easy to diff. Clients should still deserialize by key rather than position. Ordering guarantees do not apply to map-like custom scalars such as JSON.

77. What is the difference between a schema registry and schema introspection?

Introspection queries a running server for its current schema, while a registry stores versions of the schema over time with change history. Registries enable breaking-change detection, usage analytics, and coordinated rollouts. Introspection is the runtime view and may be disabled in production. Most teams publish SDL to a registry in CI.

78. What is a breaking change in GraphQL?

Removing or renaming a field, changing a type, making a nullable argument required, or tightening nullability all break existing clients. Removing an enum value or input field can also break documents that reference it. Adding a required argument without a default is breaking for the same reason. Additive changes with defaults are generally safe.

79. Can a GraphQL API return different shapes for the same field?

Only through abstract types: a field declared as an interface or union may return any implementing or member type. The response then includes __typename and only the fields selected via fragments. For concrete object types the shape is fixed by the selection, not by runtime data. This predictability is what allows codegen and normalized caching.

80. What is a field resolver's default behavior if none is provided?

Most server libraries use a default resolver that reads a property with the same name from the parent object. If the parent value has that property, it is returned without custom code. You only write resolvers for fields needing computation, renaming, or external fetches. This keeps resolver maps small for straightforward data.

81. How would you expose a computed field such as a full name?

Declare the field in the schema and add a resolver that derives the value from the parent object. Keep the computation cheap and side-effect free, since it runs per requested object. If it is expensive, cache or precompute it. Computed fields should be nullable or guaranteed by the data.

User: { fullName: (user) => `${user.firstName} ${user.lastName}` }

82. What is the difference between a query variable default and an argument default?

An argument default lives in the schema and applies whenever the client omits the argument. A variable default lives in the operation document and applies when the variables object omits that variable. If the variable is provided as null, neither default applies unless the argument is nullable. Understanding the difference prevents surprising behavior in optional filters.

83. Why is passing an explicit null different from omitting a variable?

Omitting a variable lets the operation or argument default take effect. Passing null supplies a real null value, which is valid only for nullable positions. For a non-null variable, an explicit null is a request error. This distinction matters for partial updates where "clear the field" and "leave unchanged" differ.

84. What is a GraphQL operation and which keywords define one?

An operation is an executable request document containing exactly one of query, mutation, or subscription, an optional name, optional variable definitions, and a selection set. The keyword determines which root type the top-level fields come from. Query is the default when the keyword is omitted. Directives may be attached to the operation itself.

query GetMe($withPosts: Boolean!) {
  me { id posts @include(if: $withPosts) { title } }
}

85. What is a selection set in GraphQL?

A selection set is the braced group of fields, fragments, and directives requested on an object, interface, or union. Every field returning a composite type must have a selection set, while leaf fields must not. Selection sets are what give the client control over response shape. They can nest as deeply as the schema allows.

86. Why can't a subscription field be deeply nested?

A subscription must have exactly one root field, and that root field becomes the event source. Its selection set describes the payload shape but does not create additional independent streams. Nested fields are resolved when each event fires, not subscribed to separately. This keeps the event model simple and predictable.

87. What is a resolver map and how is it organized?

A resolver map is an object keyed by type name, then by field name, with each entry a resolver function. Only fields needing custom logic must appear; others fall back to property lookup. Root types such as Query and Mutation are keys like any other type. Abstract types also get a __resolveType entry.

const resolvers = {
  Query: { posts: () => db.posts.all() },
  Post: { author: (post) => db.users.find(post.authorId) }
};

88. How do you unit test GraphQL resolvers?

Call resolver functions directly with fake parent, args, context, and info values to test logic in isolation. For integration coverage, execute a real document against the schema with a mocked or in-memory data source. Assert on both data and errors, including error codes and paths. Golden-file schema snapshots catch accidental contract changes.

89. Why validate the schema when the server starts?

Startup validation catches undefined types, missing implementations, invalid argument types, and orphaned references before any request arrives. Failing fast avoids a running server that rejects every query. Many tools also print the schema and check for naming or description lint rules. In CI the same validation prevents deploying a broken contract.

90. What is a GraphQL gateway?

A gateway exposes one GraphQL endpoint in front of multiple services and routes each field to the service that owns it. It composes a single schema from the underlying subschemas so clients see one graph. Gateways centralize authentication, cost limits, and observability. They add a network hop and a new failure mode that must be monitored.

91. What is Apollo Federation at a high level?

Federation lets several independently deployed GraphQL services contribute types to one composed supergraph. Entities are shared across services by a key such as @key(fields: "id"), and the router joins them. Each subgraph owns its own fields and resolvers. This scales schema ownership without a central monolith.

type Product @key(fields: "id") {
  id: ID!
  price: Int!
}

92. What is schema stitching and how does it differ from federation?

Schema stitching merges remote schemas inside a single gateway service, with explicit merge logic for overlapping types. Federation instead distributes type ownership to subgraphs that cooperate through entity keys and a router. Stitching is more flexible but centralized, while federation requires each service to adopt federation conventions. New projects usually prefer federation.

93. What are @defer and @stream?

@defer marks a fragment to be delivered later in a multi-part response, and @stream delivers list items incrementally. Both improve perceived performance by sending critical data first. They require transport support for incremental delivery such as multipart HTTP. Support varies by server and client, so check compatibility before relying on them.

query { product { name ... @defer { reviews { body } } } }

94. How does GraphQL compare with gRPC?

GraphQL is a client-driven query language over HTTP with a typed schema and flexible selections, while gRPC is a binary RPC framework using protobuf and HTTP/2 with fixed method contracts. gRPC is typically faster and better for internal service-to-service calls with streaming. GraphQL is friendlier for varied clients such as web and mobile apps. Many architectures use both.

95. Which transports are used for GraphQL subscriptions?

Subscriptions commonly run over WebSocket using protocols such as graphql-ws, which replaced the older subscriptions-transport-ws. Server-sent events are an alternative for one-way streams and work well through proxies. Plain HTTP polling is sometimes used as a fallback. The schema does not define the transport, so server and client must agree on one.

96. How are file uploads handled in GraphQL?

The specification has no upload type, so most implementations use the multipart request spec with a Upload scalar mapped to a file part. The mutation takes an upload argument and returns metadata or a URL. Many teams instead upload directly to object storage and pass the resulting key to a mutation, which is simpler and scales better. Either way, validate file size and type server-side.

mutation ($file: Upload!) { uploadAvatar(file: $file) { url } }

97. What is a query allowlist?

An allowlist restricts execution to a known set of operations, usually identified by persisted query hashes. Anything not on the list is rejected before parsing expensive work. This blocks arbitrary introspection or abusive ad-hoc queries from untrusted clients. It works best with build-time extraction of every client operation.

98. How is authentication usually handled for a GraphQL API?

Authentication happens at the transport layer, typically with a bearer token in the Authorization header verified by middleware before execution. The resulting identity is placed in context for resolvers to use. Because every operation shares one endpoint, per-field authorization is enforced in resolvers rather than routes. WebSocket connections must be authenticated during the handshake.

99. What is the difference between a request error and a field error?

A request error prevents execution entirely, such as a syntax error, validation failure, or malformed variables, and the response has no data. A field error occurs during execution and is recorded with a path while other fields may still succeed. The specification defines these categories so clients know whether partial data is meaningful. Both appear in the errors array.

100. What is the mutation payload pattern with user errors?

Instead of throwing for expected business failures, the mutation returns a payload type containing the result plus a list of typed user errors. Clients can then handle validation problems without parsing exception messages. Genuine bugs and infrastructure failures still surface as GraphQL errors. This keeps expected outcomes in the schema.

type CreateUserPayload {
  user: User
  userErrors: [UserError!]!
}

101. Why return a dedicated payload type from mutations instead of the raw object?

A payload type can carry the changed object, user errors, and metadata such as a client mutation ID. It can be extended later without changing the mutation's return type, which keeps the schema backward compatible. It also gives clients a stable place to read partial outcomes. Returning the bare object is simpler but harder to evolve.

102. What makes a mutation idempotent and why does it matter?

An idempotent mutation produces the same end state when repeated, often because the client sends a unique request key that the server deduplicates. Networks retry, and clients sometimes resubmit after a timeout, so non-idempotent creates can duplicate records. Accepting an idempotency key and returning the original result makes retries safe. This is design work in the resolver, not something GraphQL provides.

103. How do you update the Apollo Client cache after a mutation?

If the mutation returns the mutated object with __typename and id, Apollo merges it into the normalized cache automatically. For list additions or removals, use the update function with cache modifiers such as cache.modify or cache.evict. Alternatively, refetchQueries reloads affected queries at the cost of extra requests. Choose based on how predictable the change is.

update(cache, { data }) {
  cache.modify({ fields: { posts: (existing = []) => [...existing, data.createPost.post] } });
}

104. What is optimistic UI and how does it relate to GraphQL?

Optimistic UI renders the expected result immediately while the mutation is in flight, then reconciles with the server response or rolls back on error. Apollo Client supports this through an optimisticResponse or an optimistic cache update. It makes interactions feel instant but requires the predicted data to be close to reality. Sensitive actions such as payments usually skip it.

105. When would you use refetchQueries?

Use it when a mutation changes data in a way the cache cannot infer, such as a filtered list whose membership changed. It re-runs the named or active queries after the mutation resolves. It is simple and correct but costs extra network round trips. Targeted cache updates are usually faster once the data shape is well understood.

106. What do Apollo Client fetch policies control?

Fetch policies decide whether a query reads from the cache, the network, or both. cache-first is the default and avoids requests when data exists, network-only always fetches, and cache-and-network returns cached data then refreshes. no-cache skips writing the result. Choose per query based on freshness requirements.

107. What is normalized caching?

A normalized cache stores each object once under a key derived from __typename and id, and stores references elsewhere. Updating one object therefore refreshes every query that references it. This is powerful for consistency but requires stable IDs and correct typename data. Objects without IDs are stored by query path and are harder to share.

108. How does Apollo Client cache paginated results?

By default each combination of field and arguments is a separate cache entry, so posts(first: 10) and posts(first: 10, after: "x") are distinct. Clients either use a keyArgs configuration or field policies to merge pages into one list. Relay-style connections need explicit merge logic to append edges. Without it, pages overwrite each other.

109. What is fragment colocation or fragment masking?

Colocation keeps each component's data requirements in a fragment next to that component, and the parent composes them into its query. Masking prevents a component from reading fields it did not declare, which keeps changes local. Frameworks such as Relay enforce this strictly, while Apollo supports it through fragment registry patterns. The result is better separation of concerns and safer refactors.

110. What are the rules for a fragment's type condition?

The type condition must name a composite type: an object, interface, or union. A fragment can only be spread where its condition overlaps the parent type, otherwise validation fails. Object conditions must match exactly or be a possible member of the parent abstract type. Interface conditions can be spread on implementers.

111. Can fragments reference each other in a cycle?

No. Fragment spread cycles are a validation error because they would create an infinitely recursive selection. GraphQL detects cycles across the spread graph, including indirect ones through several fragments. Recursive data must instead be selected to a finite depth in the document. This is separate from self-referencing types, which are legal.

112. Can a variable be used as a field name or type name?

No. Variables can only supply values in argument positions or input object fields, and they are typed by the document, not by runtime data. Field names, type names, and fragment names must be static so the document can be validated ahead of execution. Dynamic field selection is a sign the schema should be redesigned.

113. Can a variable supply an enum value?

Yes, when the variable is declared with the enum type, for example $role: Role!, and the JSON value is the enum name as a string. The server validates that the string is a defined enum value. Using a String variable where an enum is expected fails validation. This keeps enum inputs flexible without literals.

{ "role": "ADMIN" }

114. Why doesn't GraphQL support wildcard selection like SELECT *?

GraphQL requires every field to be named explicitly so the response shape is predictable and validated. Wildcards would break the guarantee that clients control payload size and would complicate schema evolution. Adding a field to a type never silently changes existing responses. Clients that want everything must list it, usually through fragments.

115. What is the risk of leaving introspection enabled in production?

Introspection reveals every type, field, argument, and deprecation to anyone who can reach the endpoint, which helps attackers map the API. It also enables expensive schema queries if unbounded. Many teams disable it publicly and keep it behind authentication for tooling. Note that disabling it is not a substitute for authorization, since field names can still be guessed.

116. Can list modifiers be nested, such as [[Int!]]!?

Yes, wrapper types can nest arbitrarily. [[Int!]]! is a required list whose items are nullable lists of required integers. Each bracket level adds a list wrapper and each ! adds a non-null wrapper. Nested lists are rare but valid, for example matrix data.

117. Can a non-null list contain null elements?

It depends on whether the element type itself is non-null. [String!]! cannot contain nulls because the element type is non-null. [String]! is a required list that may still contain null entries. The two ! positions are independent and mean different things.

118. How would you model a heterogeneous activity feed?

Define an interface or union of the possible activity types, each with a shared field such as createdAt or id. Clients select common fields directly and type-specific fields through inline fragments. Include __typename so the UI can branch. Interfaces work well when every entry shares meaningful fields.

union Activity = PostCreated | CommentAdded | UserFollowed

119. What does the info argument of a resolver provide?

info describes the field being resolved, including the field name, path, return type, parent type, and the operation AST. It also exposes the schema and fragments, which enables advanced patterns such as projecting a SQL query from the selection. Most resolvers ignore it, and heavy use couples resolvers to query shape. Prefer DataLoader and simpler patterns first.

120. Why does CORS matter for a GraphQL endpoint?

Browser clients call the API cross-origin during development or from a separate app domain, so the server must allow the origin, methods, and the Authorization and Content-Type headers. Preflight OPTIONS requests must be handled, including for POST. A misconfigured CORS policy shows up as a browser-side failure even though the server is healthy. Restrict origins rather than allowing all in production.

121. What are the main drawbacks of GraphQL?

GraphQL shifts complexity to the server: query cost control, N+1 queries, caching, and authorization all become harder. HTTP caching is limited because most requests are POSTs with varying bodies. Clients need more tooling, and simple CRUD APIs may not justify the overhead. Public APIs also face discoverability and abuse concerns.

122. When should a team avoid GraphQL?

Avoid it when the API is a small, stable set of endpoints consumed by one client, or when binary performance and strict contracts matter more than flexible selection. It is also a poor fit for pure file transfer or heavy caching of static resources. The operational cost of running a graph only pays off with varied clients and evolving data needs.

123. What are the advantages of a single GraphQL endpoint?

One endpoint simplifies client configuration, authentication, rate limiting, and observability. It also lets the schema evolve without clients tracking new URLs. Cross-cutting concerns such as tracing and cost analysis are implemented once. The trade-off is that routing and caching logic must move inside the server.

124. Which methods must a custom scalar implement?

It must serialize an internal value for the response, parse a variable value from JSON, and parse a literal from the query document. All three must agree on accepted values, otherwise literals and variables behave differently. Invalid input should raise an error rather than silently coercing. Well-tested scalars cover edge cases such as time zones and precision.

new GraphQLScalarType({
  name: "DateTime",
  serialize: (v) => v.toISOString(),
  parseValue: (v) => new Date(v),
  parseLiteral: (ast) => new Date(ast.value)
});

125. How would you validate an email address with a custom scalar?

Declare a scalar such as Email and reject values that fail a validation check in both parseValue and parseLiteral. Return a clear error message so clients can surface it. This centralizes the rule instead of repeating regex checks in resolvers. Keep the rule pragmatic rather than attempting full RFC compliance.

126. How do you handle localization in a GraphQL schema?

Common approaches are a locale argument on translatable fields, a locale in the request context derived from headers, or separate fields per locale. Arguments make the choice explicit and cacheable per locale; context keeps documents simpler but hides the dependency. Whichever you choose, document it and keep default behavior predictable. Avoid returning mixed locales in one response.

type Query { product(id: ID!, locale: String = "en"): Product }

127. How do you signal "not found" from a single-object query?

Return null for a nullable field and let the client treat it as absent, which is simple but loses the reason. Alternatively return a result union of success and typed errors so the client must handle each case explicitly. Throwing an error is appropriate when absence is exceptional rather than expected. Whichever pattern you pick, apply it consistently across the schema.

type Query { user(id: ID!): User }

128. How should monetary amounts be represented in a schema?

Prefer integer minor units, such as cents, together with a currency code, because floating point rounding corrupts money. A custom scalar can encapsulate the pair if you need a single field. Document the unit in the field description to prevent misreading. Never use Float for prices.

type Price { amountCents: Int! currency: String! }

129. How should timestamps be represented in a GraphQL schema?

Use a custom DateTime scalar that serializes to ISO 8601 with an explicit offset or UTC. Avoid unix integers without documentation, and avoid locale-formatted strings. Be explicit about whether the value is an instant or a local date, which needs its own scalar. Consistency across the schema matters more than the exact format.

scalar DateTime
type Post { createdAt: DateTime! }

130. What is the difference between ID and String?

ID signals a unique identifier and serializes as a string, but it accepts both string and integer input literals. String is plain text with no identifier semantics. Using ID documents intent and lets clients treat it as opaque. Neither type validates format, so ULID or UUID rules need a custom scalar.

131. Why do some schemas expose a node(id: ID!) field?

A node field returns any object implementing a global Node interface by its opaque ID, which lets clients refetch arbitrary entities. It supports cache normalization and generic detail views. The server encodes the type into the ID so it can resolve the concrete object. Not every schema needs it, but it is common in Relay-style APIs.

type Query { node(id: ID!): Node }
interface Node { id: ID! }

132. What is the difference between a leaf field and a composite field?

A leaf field returns a scalar or enum and therefore must not have a selection set. A composite field returns an object, interface, or union and must have one. The distinction drives validation errors such as selecting subfields on a scalar. It also explains why every object query terminates in leaves.

133. What is the difference between query validation and query execution?

Validation checks the document against the schema without touching data: field existence, argument types, variable usage, fragment compatibility, and directive placement. Execution then resolves fields, calls resolvers, and assembles the response. Validation failures return no data, while execution failures can return partial data. Servers usually cache validation results per document.

134. What happens to variables that are declared but never used?

An unused variable is a validation error under the specification. This catches typos and stale documents after a query is edited. The rule applies per operation, so a variable used only in another operation still counts as unused. Linters also flag unused fragments for the same reason.

135. What role does code generation play in a GraphQL workflow?

Codegen reads the schema and client documents and produces typed models, hooks, and operation wrappers. This catches typos and shape mismatches at build time rather than runtime. It also keeps client code aligned when the schema changes. The schema becomes the single source of types across server and client.

136. What does a schema linter catch?

A linter enforces conventions such as naming patterns, required descriptions, no unused types, and consistent nullability. It can flag deprecated fields still referenced by clients and overly broad root fields. Rules are usually shared across teams to keep one graph coherent. Linting runs in CI before changes reach the registry.

137. How should a client handle an expired token or 401 response?

Detect the unauthorized response in the network layer, attempt a token refresh, and retry the original operation once. If refresh fails, clear credentials and route the user to sign-in. Avoid infinite retry loops by marking the request as already retried. Also handle the GraphQL-level UNAUTHENTICATED error code, since some servers return 200 with errors.

138. What is the difference between a GraphQL schema and a database schema?

A GraphQL schema describes the API surface the client can query, while a database schema describes storage structure and constraints. They often differ: GraphQL fields may aggregate multiple tables, rename columns, or expose computed values. Keeping them decoupled lets storage evolve without breaking the contract. Resolvers are the mapping layer between the two.

139. How do you expose health checks alongside a GraphQL API?

Add a plain HTTP route such as /health outside the GraphQL endpoint, or a simple root query field for readiness. Infrastructure probes expect fast, cheap responses without authentication or schema work. Keep deep dependency checks separate from liveness checks so a slow database does not restart healthy pods. Do not let probes execute user queries.

140. What does it mean that GraphQL is transport-agnostic?

The GraphQL specification defines parsing, validation, and execution over a document, not how that document travels. HTTP POST is the most common transport, but WebSockets, server-sent events, and even local function calls work too. This is why subscriptions need a separate transport convention. It also means caching and status-code behavior depend on the server implementation.

141. What exactly is a resolver in GraphQL?

A resolver is a function that produces the value for a single field in the schema. The execution engine calls it during a query, passing the parent object, the field arguments, the context, and an info object. Resolvers are the bridge between the declarative schema and whatever real data source backs it, such as a database, REST service, or in-memory map.

142. What are the four arguments a resolver receives?

The positional signature is (parent, args, context, info). parent is the value resolved for the enclosing field, args holds the coerced field arguments, context is a per-request shared object, and info describes the field and the query being executed.

const resolvers = {
  Query: {
    user: (parent, args, context, info) => context.db.users.findById(args.id)
  }
};

143. What is the parent value in a resolver, and why is it null at the root?

The parent is the object returned by the resolver of the field that contains the current field. For root fields on Query, Mutation, or Subscription there is no enclosing object, so the parent is conventionally undefined or an empty object. Child resolvers then receive the actual data returned by their parent, which is how nested traversal works.

144. How does a default resolver behave when a field has no explicit resolver?

GraphQL uses a default resolver that looks up a property on the parent object matching the field name. If that property is a function, it is invoked with the usual resolver arguments. This means plain JavaScript objects and class instances often need no resolvers at all for simple fields.

145. Why does the default resolver sometimes fail on fields with different names than the underlying data?

The default resolver only matches the exact field name, so a schema field called createdAt will not find a database column named created_at. You fix this by writing an explicit resolver or by mapping the data before it reaches the resolver chain. Relying on the default resolver is fine only when names line up exactly.

146. What is the difference between aliasing and renaming in resolvers?

An alias changes the response key from the client side, while a field name change in the schema requires a resolver to bridge old and new names. Aliases never affect which resolver runs; the field still resolves the same way and only the JSON key differs. Resolvers care about schema field names, not client aliases.

147. What is the GraphQL context object used for?

Context is a single value created once per request and passed to every resolver in that operation. It typically carries authentication state, data loaders, database clients, and tracing identifiers. Because it is shared, it is the right place for request-scoped resources rather than module-level globals.

148. Why must context be created per request instead of once at server start?

Context often holds the authenticated user and request-specific caches, which must not leak between users. Creating it once would let one request's identity or cached entities bleed into another. Per-request creation also gives you a natural place to open and close request-scoped resources.

149. What does the info argument contain?

info describes the current field and execution state: the field name, the parent type, the path from the root, the selection set, the operation, the schema, fragments, variable values, and the root value. It is commonly used for logging, authorization checks, and building projections for database queries. It is available in every resolver but should be used sparingly because it couples resolvers to query shape.

150. What is the difference between info.fieldNodes and info.fieldName?

info.fieldName is just the string name of the field being resolved, while info.fieldNodes is the array of AST nodes selected for that field, including directives and sub-selections. Multiple field nodes appear when the same field is requested more than once and merged by validation. Inspecting fieldNodes lets a resolver see exactly which child fields the client asked for.

151. How can a resolver use info to build a selective database projection?

You walk the selection set of info.fieldNodes and collect the requested field names, then translate them into a SQL or ORM column list. This avoids fetching columns the client never asked for. Keep the mapping explicit and fall back to a safe default when the selection is not understood.

152. What happens when a resolver returns a value of the wrong type?

GraphQL coerces the returned value to the declared type, and if coercion fails it raises a field error. For example, returning a string for an Int field may be coerced if it parses, but returning an object for a scalar fails. Nullability then determines whether the error nulls the field or propagates upward.

153. How are resolver return values coerced for scalars like Int and Float?

Built-in scalars apply strict coercion: Int must be a 32-bit signed integer, Float a finite double, String a string, and Boolean a boolean. Values outside the representable range or of incompatible shape produce errors rather than silent truncation. Custom scalars decide their own serialization rules.

154. What does a resolver return for a list field, and how are single items handled?

A list field expects an iterable or array of the item type, and each element is coerced individually. Returning a single non-list value where a list is declared is a type error in most implementations. Errors in one element null that element rather than necessarily failing the whole list.

155. What happens if a resolver returns a Promise?

The executor awaits the promise and then completes the field with the resolved value. Rejected promises become field errors attached to that field's path. This is why resolver functions are usually declared async in JavaScript servers.

156. Describe how execution walks a selection set step by step.

Execution begins at the operation's root type and iterates the fields of the selection set. For each field it validates arguments, calls the resolver, coerces the result, and if the field has a sub-selection, recurses into the returned object. The result is assembled into a response tree mirroring the query shape.

157. Why does GraphQL execute sibling fields in parallel?

The specification allows fields within the same selection set to be resolved concurrently because they are independent. This improves latency when sibling fields hit different services. Implementations typically use promise batching so a single event-loop turn handles many resolvers.

158. Are mutation root fields executed in parallel?

No. Root mutation fields must execute serially in the order they appear, because each mutation may depend on the previous one. Nested fields inside a single mutation's result can still be resolved in parallel. This serial guarantee is why batching writes into one mutation is a common pattern.

159. What is the difference between validation and execution in GraphQL?

Validation is a static pass over the document using the schema, checking that fields exist, arguments and variable types match, fragments are valid, and so on. It happens before any resolver runs and rejects the whole request on failure. Execution then walks the validated operation and resolves fields.

160. Why is it useful that invalid documents never reach resolvers?

Because validation is purely structural, you can reject malformed or abusive queries cheaply and safely before touching data sources. It also means resolvers can assume field names and argument shapes are correct. The separation keeps side effects out of the validation phase.

161. What does the field selection merging validation rule enforce?

If the same response key is selected more than once, all occurrences must be mergeable: same field name and arguments, and compatible sub-selections. Conflicting arguments under one alias are rejected. This prevents ambiguous responses where two different values would compete for a single key.

162. What does the variables are input types rule check?

Every declared variable must have a type that is an input type, meaning a scalar, enum, or input object, never an object, interface, or union. It also checks that variable usages are compatible with the expected argument types, including nullability and list nesting. Mismatches are validation errors, not runtime errors.

163. What is a fragment cycle and why is it forbidden?

A fragment cycle occurs when fragments spread each other directly or indirectly, creating infinite recursion with no base case. Validation rejects such documents because execution could never terminate. Fragment spreads must form a directed acyclic graph.

164. Name several validation rules beyond fragments and variables.

Common rules include unique operation names, lone anonymous operation, known type names, fields on correct types, required arguments present, unique argument names, leaf field selections (scalars cannot have sub-selections), possible fragment spreads, and no unused variables or fragments. Each rule is independent and produces its own error messages.

165. What are the standard top-level keys of a GraphQL error response?

The errors array entries carry message, optional locations, optional path, and optional extensions. Some servers also include a legacy code field inside extensions. The shape is defined by the specification, so clients can rely on it across servers.

{
  "errors": [{
    "message": "User not found",
    "locations": [{ "line": 2, "column": 3 }],
    "path": ["user"],
    "extensions": { "code": "NOT_FOUND" }
  }]
}

166. What does the path field in an error tell you?

The path is the sequence of response keys and list indices leading to the field that failed, such as ["users", 2, "email"]. It lets clients attach the error to the exact node in the response. Path is omitted for request-level errors that occur before execution starts.

167. What belongs in extensions on an error?

Extensions hold machine-readable metadata such as an error code, a retry hint, or a trace identifier. They are the sanctioned place for custom fields because arbitrary keys at the top level of an error are not part of the spec. Many servers expose a helper to construct errors with codes.

168. Explain partial success in a GraphQL response.

A response may contain both data and errors when some fields succeeded and others failed. The successful fields are returned normally while failed ones are nulled according to nullability. Clients must handle this mixed state rather than treating any error as total failure.

169. How does null propagation work when a non-null field fails?

If a field declared non-null resolves to null or errors, the null propagates to the nearest nullable ancestor. That ancestor becomes null and the error is recorded at the original path. If the propagation reaches the root data object, the entire data value becomes null.

170. Why can a single failing non-null field wipe out a large part of the response?

Because the non-null contract means "this can never be null", the only legal response is to null the enclosing nullable object. In a deeply nested chain of non-null fields this can bubble all the way to the root. That is why non-null should be applied deliberately rather than everywhere by default.

171. What is error masking and why do production servers do it?

Error masking replaces internal exception messages and stack traces with generic messages before sending them to clients. It prevents leaking database details, file paths, or secrets. The full error is still logged server-side, often with a correlation id echoed in extensions.

172. How do you keep useful error information while masking internals?

Distinguish trusted errors you authored, such as validation or authorization failures, from unexpected exceptions. Allow the former through with their codes and messages, and replace the latter with a generic message plus a correlation id. Custom error formatting hooks in most servers make this straightforward.

173. What are the three operation types in GraphQL?

query for reads, mutation for writes with serial root execution, and subscription for long-lived event streams. Each maps to a root type in the schema, usually named Query, Mutation, and Subscription. Only query is required for a valid schema.

174. What is a schema entry point?

The entry point is the root operation type from which execution starts, normally schema { query: Query }. All reachable types must be connected from these roots, otherwise they are unreachable from the API surface. Introspection exposes these roots so tooling knows where to begin.

175. Can a schema omit the Mutation type?

Yes. A read-only schema only needs a query root, and introspection will report no mutation type. Clients and code generators handle the absence by simply not exposing mutation helpers. Attempting a mutation against such a schema is a validation error.

176. What is the difference between schema-first and code-first development?

Schema-first means writing the SDL by hand and then binding resolvers to it, so the schema is the contract and the source of truth. Code-first means building types programmatically in your language, with the SDL generated from code. Schema-first suits cross-team contracts and review workflows; code-first suits heavy type reuse and refactoring.

177. What is a practical downside of schema-first?

The SDL and the resolver map can drift because the compiler cannot fully verify that every field is implemented. Some servers add checks at startup, but missing resolvers often surface only at runtime. Teams mitigate this with generated types and integration tests.

178. What is a practical downside of code-first?

The schema can become an implicit side effect of implementation details, which makes API review harder. You also need a build step or runtime export to produce the SDL for consumers. Without discipline, internal refactors can accidentally change the public contract.

179. How do you set up a minimal Apollo Server instance?

Install the server package, define type definitions and resolvers, then instantiate the server and start it, or mount it as middleware. Apollo Server 4 exposes startStandaloneServer for quick setups. In production you usually mount expressMiddleware into an existing HTTP app.

const server = new ApolloServer({ typeDefs, resolvers });
const { url } = await startStandaloneServer(server, {
  context: async ({ req }) => ({ token: req.headers.authorization })
});

180. What is an Apollo Server plugin and when would you write one?

A plugin hooks into lifecycle events such as request start, parsing, validation, execution, and response formatting. You write one for logging, metrics, caching, or auth enforcement that must apply across all operations. Plugins are the supported extension point instead of monkey-patching internals.

181. How does context creation work in Apollo Server 4?

You pass an async context function to the integration, which receives the request and response objects and returns the context value. It runs once per request, before execution, and its return value is passed to every resolver. Throwing inside it rejects the request before any resolver runs.

182. How do you set up GraphQL Yoga?

Yoga is created from a schema and options, and it exposes a fetch-style handler usable in Node, serverless, and edge runtimes. You can create it from SDL plus resolvers or from a generated schema. Context is produced by a function that receives the incoming request.

import { createYoga, createSchema } from 'graphql-yoga';

const yoga = createYoga({
  schema: createSchema({ typeDefs, resolvers }),
  context: ({ request }) => ({ auth: request.headers.get('authorization') })
});

183. How do you set up Mercurius on Fastify?

Register the Mercurius plugin on a Fastify instance with schema and resolvers, optionally enabling GraphiQL and subscription support. It integrates with Fastify's lifecycle and uses the Fastify request as the context source. Loaders can be registered for per-request batching.

fastify.register(mercurius, {
  schema,
  resolvers,
  graphiql: true,
  context: (req) => ({ user: req.user })
});

184. How do you set up Hot Chocolate in .NET?

You register the GraphQL server in the DI container, define a Query type as a C# class, and map the endpoint in the request pipeline. Hot Chocolate is code-first by default and can also consume SDL. Resolvers are methods on annotated types, resolved through dependency injection.

builder.Services
  .AddGraphQLServer()
  .AddQueryType<Query>();

app.MapGraphQL();

185. How do you set up GraphQL.NET?

You define an ObjectGraphType with fields and resolve expressions, compose them into a Schema, and execute documents through a document executor. GraphQL.NET is code-first and often used inside ASP.NET Core. A common practice is to register the schema and executor as singletons and the document executor per request.

186. How do you set up gqlgen in Go?

You write the schema in SDL, run the gqlgen generator to produce interfaces and models, then implement the resolver methods in generated stub files. Configuration lives in gqlgen.yml, which maps schema types to Go types. Generation keeps the resolver signatures in sync with the schema.

//go:generate go run github.com/99designs/gqlgen generate

func (r *queryResolver) User(ctx context.Context, id string) (*model.User, error) {
  return r.Store.FindUser(ctx, id)
}

187. What does Apollo Client give you beyond a plain fetch call?

Apollo Client provides a normalized cache, React hooks for queries and mutations, automatic refetching, and cache updates after writes. It also handles loading and error states and can batch or deduplicate operations. For simple one-off requests a fetch wrapper is lighter, but for a data-heavy UI the cache is the main value.

188. How does the normalized cache identify objects in Apollo Client?

Each object is stored by a cache key derived from its __typename and its id field, by default Type:id. Identical objects requested through different queries then collapse into one cache entry. Fields without a usable id are stored by a query path instead, which makes them harder to update consistently.

189. Why does __typename matter so much on the client?

__typename is what allows the cache to recognize the same entity across operations and to resolve abstract types like interfaces and unions. Most clients add it automatically to every selection set. If you request it manually in a fragment, keep it consistent so cache keys do not fragment.

190. What is the default fetch policy in Apollo Client and what does it do?

The default is cache-first: it returns cached data if the query can be fully satisfied, otherwise it goes to the network. This makes navigation feel instant but can show stale data if nothing invalidates the cache. The policy can be set per query or per client.

191. When would you choose the network-only fetch policy?

Use network-only when correctness matters more than speed, such as account balances or permission-sensitive data. It bypasses the cache read but still writes the response into the cache. A softer variant is no-cache, which neither reads nor writes.

192. How does cache-and-network differ from network-only?

cache-and-network returns cached data immediately for a fast first paint, then issues a network request and updates the UI when it resolves. It gives responsiveness and freshness at the cost of an extra render and possible flicker. It is a good fit for lists that change frequently.

193. What is the difference between useQuery and useMutation in Apollo Client?

useQuery runs automatically on mount and keeps the component subscribed to cache updates. useMutation returns a mutate function you call explicitly and does not run on render. Mutations return their result directly and usually require manual cache updates or refetches.

194. How do you update the cache after a mutation in Apollo Client?

You either pass an update function that reads and writes the cache directly, or list affected queries in refetchQueries. The direct update is more efficient because it avoids a network round trip. For simple additions, an update with cache.modify on a list field works well.

const [addTodo] = useMutation(ADD_TODO, {
  update(cache, { data }) {
    cache.modify({
      fields: {
        todos: (existing = []) => [...existing, data.addTodo]
      }
    });
  }
});

195. What does the errorPolicy option control in Apollo Client?

It decides how partial results with errors are treated: none discards data on error, ignore keeps data and hides errors, and all returns both. The default is none, which is strict but can hide usable partial data. Choose based on whether the UI can render without the failed fields.

196. What are the basics of urql and how does it differ from Apollo Client?

urql is a lighter GraphQL client built around a Client, an exchange pipeline, and framework bindings such as useQuery. Its default document caching is simpler than Apollo's normalized cache, though a normalized cache exchange is available. It is often chosen for smaller bundles and easier customization of the request pipeline.

197. What is an exchange in urql?

An exchange is a middleware stage in urql's request pipeline that can inspect, transform, or short-circuit operations. Built-in exchanges handle caching, deduplication, retries, and subscriptions. You compose them in order, so placement determines whether a stage sees requests before or after the cache.

198. What is graphql-request used for?

It is a minimal client that sends a query and variables to an endpoint and returns the parsed data, with no cache or framework integration. It suits scripts, server-side jobs, and small apps that manage their own state. You add headers and error handling yourself.

import { request, gql } from 'graphql-request';

const data = await request(endpoint, gql`
  query User($id: ID!) { user(id: $id) { id name } }
`, { id: '1' });

199. What is the difference between GraphiQL, Apollo Sandbox, and Altair?

All three are interactive IDEs for exploring a schema and running operations. GraphiQL is the long-standing open-source minimal IDE often embedded in servers; Apollo Sandbox is a hosted explorer with Apollo-specific features like schema checks and traces; Altair adds environments, collections, and subscription tooling. Choice usually comes down to team tooling and whether you need offline desktop use.

200. Why must introspection often be disabled or restricted in production?

Introspection reveals the full schema shape, which helps attackers find under-protected fields and deprecated operations. Many teams disable it publicly and expose it only to internal tooling or authenticated roles. Note that disabling introspection is not a security boundary by itself; authorization still has to be enforced per field.

201. What is a schema download and how is it used?

A schema download runs an introspection query, or reads an SDL file, and writes the result into the repo, usually as schema.graphql. Code generators, linters, and mock servers consume that file. Committing it makes schema changes reviewable in pull requests.

202. What is the relay-style schema format?

It is the SDL emitted by tooling such as the GraphQL CLI, optionally including client-specific directives and normalized formatting. Some tools require a specific schema dialect to recognize connection types and directives. Keeping one canonical form avoids diffs caused by generator variations.

203. What does schema linting catch that validation does not?

Validation checks documents against a schema; linting checks the schema itself for design problems. Rules catch things like missing descriptions, non-null list items, unused types, and inconsistent naming. Linting is a design review aid, not a runtime protection.

204. Name a few common schema lint rules and why they matter.

Rules like requiring descriptions on types and fields, forbidding non-null list items that can never be empty, and enforcing that input types end in Input improve consistency. Others flag deprecated fields without reasons or enums without a fallback value. Consistent rules make the schema predictable for consumers.

205. How do you generate TypeScript types from a GraphQL schema?

GraphQL Code Generator reads the schema, and optionally your operation documents, and emits typed models and typed hooks. Running it in watch mode or as a pre-build step keeps types in sync. Schema-only generation gives you types for resolvers on the server; operation-based generation gives precise result and variable types for the client.

npx graphql-codegen --config codegen.ts

206. What is the difference between generating types for the server and for the client?

Server generation produces resolver and parent type contracts so implementations match the schema. Client generation produces types for the exact fields in each operation, including variables and results. Both can be produced from the same schema file, which keeps the two sides aligned.

207. Why is operation-based typing better than typing the whole schema on the client?

Operation-based types know exactly which fields were selected, so optional fields stay optional and partial results are represented accurately. Schema-wide types describe everything, which makes every field look potentially present. Precise types catch mismatches between the query and the component at compile time.

208. What does the Relay compiler do?

It parses GraphQL literals in your source, validates them against the schema, and generates optimized artifacts plus TypeScript or Flow types. It also enforces Relay conventions such as unique operation names and fragment colocation. The build fails on invalid queries, so mistakes surface before runtime.

209. What is a Relay fragment and how does it colocate data with a component?

A fragment declares the data a component needs, and the component reads it from a fragment reference passed by its parent. The parent spreads the fragment without knowing its contents. This keeps each component's data requirements next to the code that uses them.

const UserCard_user = graphql`
  fragment UserCard_user on User { id name avatarUrl }
`;

210. What is the Relay connection pattern?

A connection is a standard shape for paginated lists: an object with edges, each edge holding a node and a cursor, plus pageInfo. Page info exposes hasNextPage, hasPreviousPage, and the boundary cursors. This shape lets clients paginate without knowing the underlying storage.

211. What are the required arguments on a Relay connection field?

Connections typically accept first or last with after or before cursors. The spec-style schema validator expects the arguments in a specific order and a return type ending in Connection. These conventions let Relay's pagination helpers work without custom code.

212. What does the Relay node interface provide?

The Node interface exposes a global id and allows refetching any object by that id through a root node(id:) field. It gives clients a uniform way to refetch entities after mutations. Objects implementing it should return opaque, globally unique ids.

213. What does the GitHub GraphQL API demonstrate about real-world schemas?

It uses Relay-style connections everywhere, a global node lookup, cursor pagination, and strict rate limiting based on query cost. Its schema is large but consistently structured, which is why generated types and introspection-based tooling work well. It also shows how mutations return typed payloads with client mutation ids.

214. How does the GitHub API charge for queries?

It computes a cost per query based on requested nodes and pages, and deducts from an hourly point budget rather than counting raw HTTP requests. Expensive nested pagination consumes more budget. Clients can query rateLimit to inspect remaining points and reset time.

215. What is the difference between offset and cursor pagination?

Offset pagination pages with limit and offset, which is simple but drifts when rows are inserted or deleted. Cursor pagination uses an opaque pointer to a position, so results stay stable across changes. Cursors also tend to perform better on large datasets because they avoid deep offsets.

216. When is offset pagination still acceptable?

It works well for small, mostly static datasets or admin tables where jumping to a page number matters. It is also easier for users who expect page numbers in the UI. The tradeoff is that concurrent inserts can cause duplicates or skipped rows.

217. What is cursor pagination typically implemented with?

A cursor usually encodes the sort key of the last row, often base64 of a timestamp plus id, and the query filters on values greater than that key. This produces an indexed range scan instead of an offset scan. The cursor must be opaque to clients so the encoding can change.

SELECT * FROM posts
WHERE (created_at, id) < ($cursor_created_at, $cursor_id)
ORDER BY created_at DESC, id DESC
LIMIT $first;

218. How do you express pagination arguments in GraphQL by convention?

The common pattern is first with after for forward pagination and last with before for backward, returning a connection object. Enforce a maximum page size server-side so clients cannot request unbounded lists. Rejecting both first and last at once avoids ambiguous semantics.

219. What is request batching on the client?

Batching combines several operations issued in the same tick into one HTTP request, usually as a JSON array of operations. It reduces round trips and per-request overhead on high-latency networks. The response is an array of results in the same order.

220. What are the downsides of batching client requests?

One slow operation delays the whole batch, and a malformed operation can complicate error mapping. Batching also makes per-operation caching, rate limiting, and tracing harder on the server. Many teams disable it in favor of persisted queries and HTTP/2 multiplexing.

221. What is DataLoader and what problem does it solve?

DataLoader batches and caches key lookups within a single request, turning many individual resolver lookups into one query. It collects all keys requested in a tick and calls a batch function once. It must be created per request so the cache does not leak across users.

const userLoader = new DataLoader(async (ids) => {
  const users = await db.users.findByIds(ids);
  return ids.map((id) => users.find((u) => u.id === id) ?? null);
});

222. What is query whitelisting?

Whitelisting, also called an allowlist, means the server only executes operations it has previously registered, rejecting everything else. It prevents arbitrary ad-hoc queries and dramatically reduces the attack surface. It works best with persisted queries generated at build time.

223. How do persisted queries relate to whitelisting?

With persisted queries the client sends a hash instead of the full document, and the server looks up the stored operation. If only known hashes are accepted, this is effectively whitelisting. It also shrinks request size and prevents query-text tampering.

224. Can GraphQL responses be cached by HTTP caches?

Normally no, because GraphQL usually posts to a single endpoint and responses vary by query body and variables. HTTP caches key on the URL, so caching GET-based GraphQL requires encoding the operation and variables into the URL, usually with persisted queries. Response caching then must still respect per-user data.

225. What are the basics of server-side response caching for GraphQL?

You compute a cache key from the operation, variables, and often the user's identity or tenant, then store the serialized result with a TTL. Invalidation is the hard part, so caching works best for public, rarely changing data. Field-level or entity-level caches with tags give finer control.

226. How would you cache GraphQL at a CDN?

Switch to GET requests with persisted queries so each operation maps to a distinct URL, then set cache headers per operation. Public data can use long TTLs with stale-while-revalidate, while personalized fields are split into separate authenticated requests. The key rule is never to cache a response containing user-specific data at a shared edge.

227. What is the difference between CDN caching and client caching?

CDN caching stores full HTTP responses near users and is shared across all users, so it only suits non-personalized data. Client caching stores normalized entities in the browser and is per user. They complement each other: the CDN reduces origin load, the client cache reduces network calls.

228. Why does GraphQL need explicit CORS configuration?

Browsers enforce same-origin policy, so a GraphQL endpoint on another origin needs the appropriate Access-Control-Allow-Origin and preflight responses. Content-Type application/json triggers a preflight, which must allow the method and headers. Misconfiguration shows up as opaque CORS errors in the browser console.

229. How do credentials work with CORS on a GraphQL endpoint?

Cookies or HTTP auth across origins require credentials mode on the client, plus Access-Control-Allow-Credentials: true and a specific origin instead of a wildcard on the server. Using * with credentials is rejected by browsers. SameSite cookie settings also affect whether the cookie is sent.

230. What is the simplest way to handle file uploads in GraphQL?

The simplest approach is not to send the binary through GraphQL at all: upload directly to object storage with a signed URL obtained from a mutation. That keeps the GraphQL layer small and avoids large request bodies. The legacy multipart spec exists but is only partially standardized.

231. How does the multipart upload spec work when you do use it?

The client sends a multipart/form-data request where the operations field holds the GraphQL document, the map field links file parts to variable paths, and the remaining parts hold the files. The server then substitutes streams into the variables. Support differs between servers, so verify compatibility before relying on it.

232. What transport do subscriptions use, and why not plain HTTP?

Subscriptions need a long-lived bidirectional connection, typically WebSockets using the graphql-ws protocol. Plain HTTP request-response cannot push events after the response completes. Server-sent events are an alternative for one-way streaming.

233. What does a subscription resolver return?

A subscription resolver's subscribe function returns an async iterator of event payloads. Each payload becomes the root value for one execution of the selection set, and the usual field resolvers produce the message sent to the client. Filtering and authorization belong inside the iterator, not in the client.

234. Why should every operation have a name?

A named operation appears in logs, traces, and metrics, which makes debugging and cost analysis far easier. Anonymous operations are also limited to one per document by validation. Names also become the keys for persisted query registries.

query GetUserProfile($id: ID!) {
  user(id: $id) { id name }
}

235. What would you log for each GraphQL request in production?

Log the operation name and type, a hash of the document, variable shapes without sensitive values, duration, error codes, and a request id. Field-level timing is useful for finding slow resolvers. Avoid logging raw variables that may contain tokens or personal data.

236. What does graceful schema evolution mean in practice?

It means adding fields, arguments, and enum values without breaking existing clients, and deprecating before removing. Consumers are given a deprecation window and a migration path. Breaking changes are shipped as new fields rather than in-place edits.

237. Which schema changes are breaking and which are safe?

Safe changes include adding a nullable field, adding an optional argument, adding an enum value if clients tolerate unknowns, and deprecating a field. Breaking changes include removing or renaming fields, tightening nullability, adding a required argument, and removing enum values. Changing a field's type to an incompatible one is also breaking.

238. How do you deprecate a field properly?

Mark it with @deprecated(reason: "...") so introspection and IDE tooling warn consumers, and keep it working during the transition. Track usage from operation logs to know when it is safe to remove. Announce the removal date and provide the replacement field.

type User {
  name: String @deprecated(reason: "Use displayName instead")
  displayName: String!
}

239. How do you pass a value from a parent resolver down to its children?

The parent resolver returns an object, and that object becomes the parent argument of each child resolver. You can return a plain object, a class instance, or a lightweight wrapper holding ids for later loading. Children should not depend on undocumented internal properties of the parent.

240. What is a trivial resolver and when should you omit it?

A trivial resolver just returns parent.fieldName and adds no logic, which the default resolver already does. Omit it unless you need coercion, a default value, or logging. Removing noise makes the resolver map easier to review.

241. How do you implement a field that requires an expensive computation only when selected?

Leave it as its own resolver so it runs only when the client includes that field in the selection. If you compute it eagerly in the parent, every query pays the cost even when the field is not requested. This lazy-by-default behavior is one of GraphQL's main efficiency wins.

242. What happens to a resolver for a field the client did not select?

It is never called. Execution only visits fields present in the selection set, so unselected fields cost nothing beyond their type definition. This is why trimming an over-fetching query is a real performance fix.

243. How can a resolver detect whether a nested field was requested?

Inspect info.fieldNodes and look at the selection set for the child field name, accounting for fragments and inline fragments. This is common in projection optimization. Abstract types require resolving fragments by their type condition, which is why helpers exist.

244. What is the resolver map structure in most JavaScript servers?

It is an object keyed by type name, then by field name, so Query.user and User.posts live under their owning types. Root fields sit under Query and Mutation, and interface or union types use a __resolveType function. Scalar definitions use __serialize and __parseValue hooks in some implementations.

245. How do you resolve an interface or union type at runtime?

Provide a type resolver that returns the concrete type name for each value, typically reading a discriminator field or checking instanceof. Without it the server cannot know which selection set to apply and throws an error. Some servers can infer it from __typename on the object.

const resolvers = {
  SearchResult: {
    __resolveType: (obj) => (obj.title ? 'Post' : 'User')
  }
};

246. What is an abstract type in GraphQL?

Interfaces and unions are abstract because they describe a set of possible object types rather than one concrete shape. Clients select fields directly on an interface, but union members require inline fragments. Every possible member must be an object type in the same schema.

247. How do you add a custom scalar such as DateTime?

Declare it in the schema with scalar DateTime, then provide serialize, parseValue, and parseLiteral implementations. Serialization converts the internal value to JSON, while the parse functions validate inbound variables and literals. Well-tested community scalars are usually better than ad-hoc ones.

248. What is the difference between parseValue and parseLiteral for a custom scalar?

parseValue handles values arriving through variables as raw JSON, while parseLiteral handles values written inline in the query as AST nodes. Both must enforce the same validation rules. Supporting only one leads to inconsistent behavior between variables and literals.

249. How do you return an error from a resolver without throwing?

Most servers allow returning the error in the result position, or wrapping it in a union result type such as UserResult with User and NotFoundError members. Result unions make expected failures part of the schema and therefore typed for clients. Throwing is better for unexpected failures.

250. Why are result unions often preferred over throwing for expected failures?

They appear in the schema, so clients must handle them explicitly and code generation produces exhaustive types. They also avoid the null propagation surprise of a failed non-null field. The tradeoff is a slightly more verbose query shape.

251. What is a resolver-level authorization check and where should it live?

It is an explicit check inside the resolver or a schema directive that verifies the current user may read this object or field. Checks must run where the data is actually exposed, not only at the entry point, because nested fields can be reached through other paths. Centralizing them in a directive reduces omissions.

252. Why is authorization at the root query alone insufficient?

An object reachable through one authorized path may be reachable through another, so per-entry checks leave holes. Field-level checks ensure every path enforces the same rule. Attackers can also query deeply nested fields you never anticipated.

253. How do you limit query depth and complexity?

Add a validation rule that computes depth or cost from the document and rejects operations above a threshold. Cost is usually estimated per field, with multipliers for list pagination arguments. This protects against accidentally expensive or maliciously nested queries.

254. What is an introspection query and how is it used by tooling?

It is a normal GraphQL query against __schema that returns all types, fields, arguments, directives, and deprecations. IDEs, code generators, and mock servers use it to learn the API. Because it is just a query, it can be disabled like any other field.

255. What is a mock server and when is it worth setting up?

It serves a schema with generated or hand-written fake resolvers so front-end work can start before the real API exists. It also makes tests deterministic and fast. Mocks are only as good as the schema, so they must be regenerated when the contract changes.

256. How do you test a resolver in isolation?

Call the resolver function directly with a hand-built parent, args, context, and a minimal info stub, then assert on the returned value or thrown error. This avoids spinning up HTTP and keeps tests fast. Integration tests against the real schema cover wiring and coercion.

const result = await resolvers.Query.user(
  {},
  { id: '1' },
  { db: fakeDb },
  {}
);
expect(result.name).toBe('Ada');

257. What does an integration test of a GraphQL endpoint add over unit tests?

It exercises parsing, validation, execution, coercion, and error formatting together, which is where most surprises live. It also catches resolver wiring mistakes that unit tests miss. Use a small set of realistic operations rather than mirroring every unit test.

258. How do you handle N+1 queries in a GraphQL server?

Use a per-request batching loader so child resolvers collect ids and issue one query, or restructure the parent query to join the needed data. Detect the problem by logging queries per request in development. N+1 is the most common performance defect in naive GraphQL servers.

259. What is the difference between a null result and a missing field in a response?

A null result means the field was resolved to null, while a missing key means the field was not part of the selection or was omitted due to an error path. Clients should not treat absence as null automatically. On the server, always return all selected keys explicitly when possible.

260. How do fragments affect execution?

Validation expands fragment spreads into the effective selection set for each type, so execution sees a flattened shape. Inline fragments narrow which fields apply to a concrete type. Fragment reuse therefore does not cost extra requests, only a larger resolved selection.

261. What is the __typename meta field and can resolvers override it?

__typename returns the concrete object type name of the current value and is available on every object, interface, and union. Servers provide it automatically, and overriding it is not supported. Clients rely on it for cache keys and for deciding which fragments apply.

262. How do directives affect resolver execution?

Directives like @include and @skip are evaluated before a field is executed, so a skipped field's resolver never runs. Custom directives can be implemented as schema transforms or resolver wrappers. This makes directives a cheap way to conditionally shape a query.

263. What is the __schema and __type meta field used for?

They expose introspection entry points: __schema describes the whole schema and __type(name:) returns one type. They are the backbone of every explorer and codegen tool. Both can be disabled together with introspection.

264. What is the difference between a query document and an operation?

A document is the parsed file, which may contain several operations and fragment definitions. An operation is one executable definition inside it with an optional name and variable definitions. A request must identify which operation to run when the document contains more than one.

265. How are variables coerced before execution?

Incoming variable values are checked and coerced against their declared input types, including defaults and nested input objects. Coercion failures are request errors that abort execution entirely. After coercion, resolvers receive typed values rather than raw JSON.

266. What happens when a required variable is missing?

If the variable is non-null and has no default, coercion fails and the server returns a request error with no data key. If a default is declared, the default fills in. This distinction matters because request errors bypass resolvers completely.

267. How do default values on arguments differ from default values on variables?

Argument defaults live in the schema and apply when the argument is omitted, while variable defaults live in the operation and apply when the client omits the variable. A variable default is only used if the variable itself is undefined, not if it is explicitly null. Mixing the two up causes surprising null behavior.

268. How do you implement a root field that returns a computed value with no backing entity?

Write a resolver on Query that computes the value from context, configuration, or other services. There is no requirement that root fields map to stored records. Keep such fields cheap or cache them, since they can be called on every page load.

269. What is a schema transform and when do you need one?

A transform programmatically rewrites the schema, for example to add directive-based auth, rename types, or filter fields per user role. It is useful for wrapping third-party schemas you cannot edit. Transforms run at build or startup time, not per request, unless you generate per-role schemas.

270. How can you stitch or federate multiple GraphQL services?

Stitching merges schemas and delegates fields to the owning service, while federation defines entities with keys that a gateway can resolve across services. Both avoid a monolith but add operational complexity in tracing and error handling. Start with one schema unless organizational boundaries truly require splitting.

271. What is the resolver cost of a deeply nested query and how do you bound it?

Each nesting level multiplies the number of resolver calls, so depth times list size can explode. Bound it with depth limits, page size caps, and cost analysis that accounts for pagination arguments. Also enforce timeouts so a single operation cannot pin a worker.

272. How does the server know which operation to execute when a document has several?

The client sends an operationName alongside the query and variables. If omitted and multiple operations exist, the server rejects the request. Code generators and clients set it automatically from the operation's name.

273. What is a GraphQL over HTTP GET request used for?

GET is used for queries only, with the document and variables passed as query parameters, which makes responses cacheable by URL. Mutations must use POST because GET requests can be retried or prefetched. Many servers disable GET mutations for safety.

274. Why do some servers reject mutations sent over GET?

GET requests can be cached, prefetched, or replayed by proxies and browsers, which would cause unintended writes. Restricting mutations to POST prevents accidental execution. It also aligns with HTTP semantics where GET must be safe.

275. What is a mutation payload type and why do servers return one?

Rather than returning the bare entity, a mutation returns a payload object containing the changed entity plus metadata like client mutation id or user errors. This lets the response carry partial failures and extra context without breaking the return type. It also mirrors the Relay mutation convention.

276. What are userErrors in a mutation payload?

userErrors is a list of expected business-rule failures with a field path, message, and code, returned inside a successful response. It distinguishes predictable validation problems from system exceptions. Clients can then highlight the offending input field.

277. How do you handle optimistic UI with a GraphQL client?

The client writes an optimistic response into the cache immediately, then reconciles when the server result arrives or rolls back on error. This requires the optimistic object to have a plausible cache key, often a temporary id. Apollo and urql both support this pattern.

278. How do you keep a subscription connection healthy through proxies?

Use ping and pong keepalives at an interval shorter than typical proxy idle timeouts, and reconnect with backoff on close. Many clients expose connection init payloads for auth tokens. Proxies and load balancers must also be configured to allow upgrade requests.

279. What is the difference between polling and subscriptions for live data?

Polling refetches on an interval, which is simple, stateless, and easy to cache, but it wastes requests and adds latency. Subscriptions push changes as they happen and scale better for sparse events, but require persistent connections and more infrastructure. For slow-changing data, polling is often the pragmatic choice.

280. How would you roll out a breaking schema change safely?

Add the new field alongside the old one, migrate clients, monitor usage of the old field through operation logs, then deprecate and finally remove it. Schema checks in CI catch accidental breaking changes before merge. A staged rollout with a canary reduces the blast radius.

Intermediate 340 questions

Schema design, Relay connections and pagination, mutation payloads, custom scalars, execution and resolvers, caching, N+1 and DataLoader, security limits, subscriptions, testing and client caches.

281. Why is making a field non-null in GraphQL often described as a breaking change in disguise?

A non-null field propagates errors upward: if the resolver fails, the nearest nullable ancestor becomes null instead of returning partial data. It also constrains future schema evolution, because you can never legitimately return null for that field without a breaking change. Adding nullability later is easy, but adding it up front is a promise you cannot easily withdraw.

282. What is the practical cost of marking every field non-null by default?

Any transient backend failure escalates into a null parent object, so clients lose sibling data they could have used. Teams often end up returning placeholder values like empty strings or zero to satisfy the type, which hides real problems. The safer default is to mark only fields that are truly invariant, such as identifiers.

283. When should you use a list of nullable items versus a non-null list of non-null items?

[Item!]! guarantees every element exists and the list itself exists, which is the strongest contract and the most brittle. [Item] allows both null elements and a null list, which is rarely what you want. [Item!] lets the list exist while individual lookups may fail, which is a common compromise for batched lookups.

284. What does [Item]! communicate to a client, and what does it still not guarantee?

It guarantees the list itself is always present, so clients can iterate without a null check on the collection. It does not guarantee the contents: individual entries may be null, so clients must still null-check each element. This is useful when element resolution can fail independently but the collection itself is always known.

285. How does error bubbling work when a non-null field throws?

The error is recorded in the errors array and the field's value position is treated as null. Because the parent field declared non-null children, that null violates the parent's type, so nulling propagates to the nearest nullable ancestor. Everything below that ancestor is lost, even fields that resolved successfully.

286. Why is adding a required argument to an existing field a breaking change?

Existing queries that call the field without that argument become invalid at validation time. You can add a non-null argument only if it also has a default value, which keeps old documents valid. Otherwise the argument must be nullable or introduced on a new field.

287. How can you add a non-null output field to an existing type without breaking clients?

You cannot add a truly non-null field safely unless the resolver can always supply a value for every existing object. In practice you add it as nullable, populate it everywhere, monitor usage of null, and only then promote it. Promotion later is still technically breaking for clients that assumed the nullable shape, so coordinate it as a versioned change.

288. What is the safest way to remove a field from a schema?

Deprecate it first with a reason, measure usage over a full release cycle, and only remove it after clients confirm they no longer send it. Removal is always breaking for any client still selecting it. Removing an unused field from a private, single-client schema is fine once you verify no persisted queries reference it.

289. Which names are effectively reserved in a GraphQL schema?

Names beginning with double underscores are reserved for introspection, so __typename, __schema, and __type cannot be redefined. Type names must not start with __, and built-in scalars such as String and Int should not be shadowed. Argument names after, before, first, last carry meaning in Relay connections and should be reserved for that purpose.

290. What naming conventions are common for GraphQL types, fields, and enums?

Types use PascalCase, fields and arguments use camelCase, and enum values use SCREAMING_SNAKE_CASE. Input types are usually suffixed with Input and payload types with Payload. Consistency matters more than the specific choice, because code generators and clients derive names mechanically from the schema.

291. Why should a GraphQL API avoid exposing database table names directly?

Table names leak storage details and couple the public contract to internal refactoring such as renaming or sharding. A domain-oriented type name lets the persistence layer change without a schema change. It also lets you model relationships the database does not express directly.

292. What are the four core parts of a Relay connection type?

A connection exposes edges, a list of edge objects, and pageInfo with pagination metadata. Each edge contains a cursor and a node, the actual item. Some schemas also add totalCount. The shape is standardized so generic client pagination logic works across fields.

293. What is the purpose of the pageInfo object in a connection?

It carries hasNextPage, hasPreviousPage, startCursor, and endCursor. Clients use the flags to decide whether to show pagination controls and the cursors as anchors for the next request. It avoids the client having to inspect the edge list to infer more pages.

294. Why does the Relay spec separate edges from nodes instead of returning a bare list?

Edges let you attach relationship-specific metadata, such as a cursor, a role, or a "joined at" timestamp, without polluting the node type. A bare list cannot carry per-relationship data. It also gives a stable place to evolve pagination details.

295. What makes a good cursor?

A cursor should be opaque to clients, stable across queries, and uniquely identify a position in a deterministic ordering. It is commonly a Base64 encoding of a composite key such as a timestamp plus an ID. It must not be a page offset if the underlying data can change between requests.

296. Why is a plain offset a poor cursor?

Offsets shift when rows are inserted or deleted, so a client paging through results can see duplicates or skip records. They also degrade in performance on large tables because the database must scan to the offset. A keyset cursor based on the sort key is stable and index-friendly.

297. How do you build a composite cursor for a sort with ties?

Encode every column in the sort plus a unique tiebreaker such as the primary key. The "after" predicate becomes a lexicographic comparison over that tuple. This guarantees a total order and therefore reproducible pagination.

cursor = base64("createdAt:2024-05-01T10:00:00Z|id:842")

298. Should cursors be encrypted or signed?

Signing prevents clients from forging or tampering with cursors and protects against injection if the decoded value reaches a query builder. Opacity alone is not security, since Base64 is trivially decoded. Signing adds a small cost but removes a class of attacks, so it is worthwhile for public APIs.

299. How does forward pagination with first and after work?

first limits how many items to return and after gives the cursor to start strictly after. The server returns items in order plus pageInfo.endCursor for the next request. hasNextPage is computed by checking whether at least one more row exists beyond the returned window.

300. How does backward pagination with last and before differ?

last returns the final N items before the given cursor, and results are usually still returned in forward order for consistency. Computing hasPreviousPage requires a look behind the window. Backward pagination is harder to implement efficiently because most indexes are optimized for ascending scans.

301. What are the exact semantics of hasNextPage and hasPreviousPage?

hasNextPage means more items exist after the last returned edge in the current sort order. hasPreviousPage means more items exist before the first returned edge. The spec notes that for a first-based query hasPreviousPage may be unreliable unless the server computes it explicitly.

302. Why is hasPreviousPage often returned as false when using first?

Answering it truthfully requires an extra query to check whether anything exists before the window, which costs a round trip. Many implementations only guarantee the flag in the direction being paginated. Documenting this limitation is better than returning a misleading value.

303. How do you implement hasNextPage without a separate count query?

Fetch first + 1 rows and return only first. If the extra row exists, there is a next page. This is a single indexed query and avoids a full count, which is the main reason to prefer it over totalCount.

304. What are the pitfalls of offset pagination in a public API?

Results drift as data changes, deep offsets are slow, and large offsets can be abused to scan the whole table. It also interacts badly with non-deterministic ordering unless a tiebreaker is added. It remains acceptable for small, static, or admin-only datasets.

305. What are the trade-offs of exposing totalCount on a connection?

It is convenient for pagination UI but requires a count query that can be expensive on large or filtered sets. It also invites clients to compute page numbers, which conflicts with cursor semantics. Many teams make it optional or resolve it lazily only when selected.

306. How can you make totalCount cheap when it is rarely used?

Resolve it lazily so the count runs only if the field is in the selection set. You can also cache counts per filter for a short TTL or use an approximate count from database statistics. Some APIs expose it behind a separate field so the intent is explicit.

307. What is the difference between filtering with one argument per field and a single filter input object?

Per-field arguments are simple and discoverable for two or three filters but grow unwieldy and pollute the field signature. A filter input groups criteria, is reusable across fields, and is easier to extend without changing the field's argument list. It also supports nesting for logical operators.

308. How would you design a filter input that supports AND and OR logic?

Use a recursive input type with a list of and and or conditions plus scalar predicates. Recursive inputs are legal in GraphQL as long as the recursion passes through a list or nullable field. Bound the depth server-side to prevent abusive queries.

input UserFilter {
  name: StringFilter
  and: [UserFilter!]
  or: [UserFilter!]
}

309. Why should filter inputs avoid a free-form JSON scalar?

A JSON scalar gives no type safety, no introspection, and no validation, so clients discover filters by trial and error. It also makes query cost analysis and caching harder. Typed inputs keep the contract explicit and enable code generation.

310. What is a sensible convention for sort arguments on a connection?

Use an enum for the sortable fields and a separate order enum for ascending or descending, rather than a string like "createdAt DESC". Enums are validated and introspectable. Allow multiple sort keys as a list if tie-breaking matters.

311. Why must every paginated field have a deterministic total order?

Without a unique tiebreaker, two rows with equal sort values can appear in different orders across queries, which breaks cursor pagination. Always append a unique column such as the ID to the sort. This also makes the query plan stable and cacheable.

312. How do you expose a search capability alongside structured filters?

Add a dedicated argument or a search field in the filter input, and keep it separate from exact-match predicates. Document the matching semantics, such as prefix versus full-text, because clients cannot infer them. Search results should still be paginated with a stable order, often by relevance plus ID.

313. What makes a good input type for a create mutation?

Group all writable fields into a single CreateXInput with only the fields the client may set, never the server-generated ID or timestamps. Mark truly required fields non-null and leave optional ones nullable so partial updates are expressible. Reuse the same input for related mutations where semantics match.

314. How should an update mutation distinguish "set to null" from "leave unchanged"?

With a plain nullable input field these are indistinguishable, since an absent key and an explicit null look the same after coercion. Solve it with a wrapper input such as { value: ... }, or use separate patch semantics with a dedicated clearField flag. Document the chosen convention clearly because it affects generated client types.

315. What is the purpose of clientMutationId in a mutation payload?

It is an opaque string the client sends and the server echoes back, letting the client correlate a response with the request it issued. This matters when multiple mutations are in flight or when responses arrive out of order. Relay uses it for optimistic update bookkeeping.

316. What is a good payload shape for a mutation?

Return a wrapper type containing the affected entity, any user-facing errors, and the correlation field. A wrapper leaves room to add fields later without changing the mutation signature. Returning the bare entity directly makes future extension breaking.

type CreateUserPayload {
  user: User
  errors: [UserError!]!
  clientMutationId: String
}

317. When would you use a union in a mutation result?

A union expresses mutually exclusive outcomes such as CreateUserSuccess or ValidationFailure in a type-safe way. Clients select fields per variant using inline fragments, so the compiler knows exactly what is available. Use it when failure shapes differ substantially, not for a single generic error list.

318. How do domain errors differ from transport errors in GraphQL?

Domain errors are expected business outcomes, such as an email already in use, and belong in the payload's data. Transport errors are infrastructure or protocol failures, such as a timeout or a malformed query, and belong in the top-level errors array. Mixing them forces clients to parse error strings to distinguish recoverable cases.

319. Why is putting validation failures in the top-level errors array problematic?

The errors array loses strict typing, so clients must parse messages or rely on unstable extensions. It also cannot be modeled as partial data, and some clients treat any error as a hard failure and skip rendering. Typed payload errors keep the contract in the schema.

320. How can a mutation be made idempotent?

Accept a client-generated idempotency key and store the result keyed by it, returning the stored result on retry. Alternatively use natural keys with upsert semantics. This protects against duplicate charges or duplicate records when a network retry occurs.

321. Why are mutations inherently unsafe to retry without idempotency support?

A timeout does not tell the client whether the server committed the change, so a blind retry can duplicate it. GraphQL over HTTP has no built-in deduplication. The server must own idempotency through a key or a naturally idempotent operation design.

322. How do mutation responses support optimistic UI?

The client applies a predicted result immediately, then reconciles using the real payload returned by the mutation. Returning the full affected entity, including server-generated fields and updated timestamps, makes reconciliation accurate. A thin payload forces a refetch and causes visible flicker.

323. What should a mutation return if the client needs the updated list order?

Return the affected entity plus enough connection metadata, such as the new edge and cursor, to insert it in the right position. Otherwise the client must refetch the whole connection. Relay handles this with connection edge updates in the payload.

324. What are the pitfalls of a custom DateTime scalar?

If serialization format is not documented, clients guess between epoch milliseconds, ISO strings, and local times, causing silent timezone bugs. Pick ISO 8601 with an explicit offset or Z, and validate on input. Also decide whether the scalar accepts only strings or numbers.

325. Why is a custom JSON scalar often considered an escape hatch?

It bypasses the type system, so clients cannot introspect the shape and the server cannot validate it. It also weakens query cost analysis and caching. It is reasonable for genuinely schemaless metadata, but not as a shortcut for modeling known structures.

326. What should a UUID scalar validate on input?

It should reject anything that is not a canonical UUID string and normalize case if the storage layer is case-sensitive. Serialization should be a lowercase string for consistency. This prevents malformed IDs from reaching the database and producing confusing errors.

327. Why is a Decimal scalar preferable to Float for money?

Float is IEEE 754 binary floating point and cannot represent values like 0.10 exactly, so rounding errors accumulate. A decimal scalar transmits a string and preserves precision end to end. Never use Int for currency unless you define minor units explicitly.

328. What is scalar coercion and where does it happen?

Coercion converts external input values, such as JSON literals or variables, into the scalar's internal representation, and serializes internal values back to JSON on output. Input coercion happens during validation and execution, output serialization during result formatting. Custom scalars must implement both directions.

329. What happens if a custom scalar's serializer throws?

That field is treated as errored and the error appears in the errors array, with null propagating according to nullability. A throwing serializer on a non-null field can null out a large part of the response. Validate data at write time so serialization cannot fail.

330. Why prefer enums over string literals for a fixed set of values?

Enums are validated at query time, so a typo fails fast instead of reaching the resolver. They are introspectable, which enables code generation and better tooling. Strings are more forgiving for forward compatibility but push validation into application code.

331. What is the risk of removing an enum value?

Any client that sends or pattern-matches on that value breaks, and persisted queries may still reference it. Deprecate the value first and keep it resolvable during a transition. Removing it is a breaking change even though it feels like a small edit.

332. How do you handle an enum value returned by the server that the client does not know?

Well-behaved clients should tolerate unknown enum values and fall back to a default rendering. This is why some teams prefer strings for open sets that grow frequently. If you use an enum, treat unknown values defensively in generated code.

333. When should you model polymorphism with an interface instead of a union?

An interface is right when variants share fields that clients query generically, such as id and createdAt on a Node. A union is right when variants share nothing and clients must branch. Interfaces give you shared field selection without inline fragments for the common parts.

334. Can a type implement multiple interfaces, and what does that buy you?

Yes, a type may implement several interfaces, which lets one object be queried through different abstractions such as Node and Timestamped. It composes contracts without inheritance. Every interface field must be present on the implementing type with a compatible type.

335. What is __typename and why is it important for abstract types?

It is an implicit field returning the concrete object type name at runtime. Clients need it to know which inline fragment actually applied, and normalizing caches use it as part of the cache key. It is available on every object, interface, and union selection.

336. How does a GraphQL server determine the concrete type of an abstract value?

The executor calls the interface or union's resolveType function, which inspects the resolved value and returns a type name. If it returns null or an unrelated type, an execution error occurs. The default implementation may look for a __typename property on the value.

337. What are common bugs in a resolveType implementation?

Returning a type name that is not a possible type of the abstract type causes a runtime error. Returning null for a non-null abstract field bubbles the error upward. Relying on a discriminator field that is missing on legacy data is another frequent failure.

338. Why can querying an interface field return a type the client did not expect?

Interfaces are open: the server may add a new implementing type at any time, and existing queries still validate. A client matching exhaustively on known types can therefore miss the new one. Always keep a default branch in type switches.

339. What does the built-in @deprecated directive do?

It marks a field, argument, enum value, or input field as deprecated with an optional reason, and it appears in introspection so tooling can warn. It does not block execution or affect validation. It is purely a communication mechanism.

field(arg: String @deprecated(reason: "Use newArg")): String

340. What is @specifiedBy used for?

It attaches a URL to a custom scalar declaring the specification it implements, such as a particular date-time format. Tooling can surface that link to developers. It documents semantics that introspection otherwise cannot express.

341. What does the @oneOf directive mean on an input object?

It declares that exactly one of the input's fields must be provided, which models tagged unions in input position. All fields must be nullable, and the server enforces the single-key constraint at coercion. It is useful for lookup-by-id-or-email style arguments.

342. What are the allowed locations for a directive?

Directives declare locations such as FIELD, FRAGMENT_SPREAD, INLINE_FRAGMENT, OBJECT, ARGUMENT_DEFINITION, and SCHEMA. A directive used in the wrong location is a validation error, not a runtime surprise. Locations should be the narrowest set that serves the use case.

343. Why can a custom directive not change the shape of a field's return type?

Directives are metadata; the type system is fixed at schema build time, so a directive cannot alter validation or the declared type. Any behavior change must happen in execution logic, such as wrapping resolvers. Attempting runtime type changes breaks clients and tooling that trust introspection.

344. How would you implement an @auth directive on fields?

Define it with a required requires argument and apply it at FIELD_DEFINITION. At schema build time, wrap the resolver so it checks the context's authenticated principal before delegating. Keep the directive declarative and enforce it centrally rather than in each resolver.

directive @auth(requires: Role!) on FIELD_DEFINITION

345. What is a risk of implementing authorization only with schema directives?

Directives applied at a field definition cannot see per-object ownership, so they handle role checks but not row-level rules. They are also easy to forget on new fields. Combine declarative role gates with explicit checks in domain logic for object-level permissions.

346. How can a directive be used for response formatting, such as case conversion?

A field-level directive can wrap the resolver and transform its output, for example upper-casing a string. This keeps formatting declarative and consistent across fields. Be careful not to hide data semantics, since formatting in transport layers can confuse consumers.

347. What is the difference between a named fragment and an inline fragment?

A named fragment is a reusable definition applied with a spread; an inline fragment is written directly inside a selection set. Inline fragments are typically used to apply a type condition or a directive locally. Named fragments are better for reuse and for colocation with components.

348. Why must a fragment spread be used only where its type condition is compatible?

GraphQL validation requires the fragment's type condition to intersect with the parent type, otherwise the spread could never match. Spreading AdminUser inside an unrelated type is a validation error. Inside a union or interface, spreads on member types are allowed and only apply when the runtime type matches.

349. What is fragment colocation and why do client frameworks encourage it?

Each component declares the fragment containing exactly the data it needs, and parents compose those fragments into their query. This keeps data requirements next to the UI that uses them, so a component change does not require hunting through a distant query. It also makes components reusable in different queries.

350. What problem do fragment arguments solve?

They allow a fragment to accept parameters, such as a size or a locale, instead of hardcoding them. Without them, teams duplicate fragments per variation. Support depends on the server and client tooling, so verify compatibility before designing around it.

351. Can a fragment reference variables?

Standard GraphQL fragments cannot declare their own variables, but they can reference variables declared by the enclosing operation. This couples the fragment to the operation's variable names. Fragment arguments were proposed to remove that coupling.

352. How do @skip and @include differ?

@skip(if: true) omits the selection, while @include(if: true) keeps it. Both take a non-null Boolean and apply at field, fragment spread, and inline fragment locations. Using both on the same selection is legal but confusing.

353. Why are @skip and @include a poor substitute for proper query shaping?

They make the document static while the response shape varies, which complicates caching and generated types. Deeply nested conditional selections also become unreadable. Prefer separate fragments or queries when the shape differs substantially.

354. What is the difference between an absent variable and an explicitly null variable?

An absent variable has no value supplied; if it has a default, the default applies, and if the argument is nullable the argument is treated as not provided. An explicit null is a provided value of null, which suppresses the default. Resolvers can distinguish these through argument presence checks.

355. When is a variable default applied?

Only when the variable is not provided at all; passing explicit null does not trigger the default. If the variable is non-null and unprovided with no default, the operation fails validation before execution. This behavior is a common source of "why is my default ignored" bugs.

356. What are GraphQL's input coercion rules for scalars?

Input values must match the expected type or be coercible per the scalar's rules; for example Int rejects fractional numbers and String rejects numbers. Non-null inputs reject both null and absence. Coercion failures are request errors that prevent execution.

357. How does list input coercion behave when a single value is supplied?

If the expected type is a list and the value is not a list, GraphQL coerces it into a single-element list. This is convenient but can hide client bugs. Explicitly passing null still means null rather than an empty list.

358. What is the difference between a validation error and an execution error?

Validation errors mean the document is invalid against the schema, such as an unknown field, and the request is rejected before any resolver runs. Execution errors happen while resolving, produce partial data plus an errors entry, and are positioned by path. Clients should handle them very differently.

359. What belongs in the extensions field of a GraphQL error?

Machine-readable metadata such as an error code, a correlation ID, a retry hint, or field-level details. It is intentionally outside the spec, so clients should treat it as optional. Keep codes stable and documented.

{ "message": "Invalid coupon", "extensions": { "code": "COUPON_INVALID" } }

360. Why should error messages not leak internal details?

Stack traces, SQL fragments, and internal hostnames help attackers and confuse users. Log the full detail server-side with a correlation ID and return a sanitized message plus that ID. This keeps debuggability without exposure.

361. What is error masking and when is it appropriate?

Error masking replaces internal error details with a generic message before sending the response. It is appropriate for unexpected exceptions in production, but not for expected domain errors that clients must handle. Always preserve the original error in logs.

362. How do you correlate a client-visible error with a server log entry?

Generate a request or trace ID in middleware, attach it to the context, and include it in every error's extensions. The same ID appears in logs and traces. Support can then find the exact request from a user report.

363. How do you deprecate a field safely in a large schema?

Add @deprecated with a reason that names the replacement, keep it resolving correctly, and track usage per client via metrics on field resolution. Announce a removal date and verify traffic has dropped before deleting. Never deprecate and remove in the same release.

364. Why is adding a field with a non-null type to an input object dangerous?

Existing clients that omit that input field will now fail validation or coercion. Make new input fields nullable or give them defaults. This is one of the most commonly missed breaking changes because the field is new.

365. What is a schema check in CI and what does it compare?

It diffs the proposed schema against the last deployed or published version and classifies changes as breaking or safe. Breaking changes fail the build unless explicitly approved. It catches removals, type changes, and new required arguments before clients are affected.

366. Which schema changes are generally non-breaking?

Adding a field to an object, adding an optional argument, adding a new type, adding a value to an output enum only if clients tolerate unknown values, and deprecating anything. Adding an input enum value is also usually safe. Loosening nullability on output is safe, tightening is not.

367. Which schema changes are breaking but often overlooked?

Making a nullable output field non-null, changing an input field from nullable to non-null, removing an enum value, renaming arguments, and changing a field's type even to a "compatible" scalar. Adding a required argument is also breaking. Each needs client coordination.

368. What does a contract-first workflow mean for GraphQL teams?

The schema is agreed and reviewed before implementation, then both server and clients generate code from it. This surfaces design problems early and lets client work proceed in parallel. It requires discipline to treat the schema as an artifact under review, not an implementation byproduct.

369. How can a mock server help during schema-first development?

It serves the agreed schema with generated or fixture data so clients can build against it before resolvers exist. This decouples frontend and backend schedules and surfaces schema gaps early. Mocks must be replaced by real resolvers before integration testing.

370. Why are schema descriptions worth writing?

Descriptions appear in introspection, so they show up in GraphiQL, generated docs, and IDE hovers. They are the only documentation most consumers read. Document units, formats, nullability meaning, and authorization requirements on sensitive fields.

371. What does a bad schema design look like in practice?

Stringly typed fields everywhere, one giant getData query, inconsistent naming, and untyped JSON blobs. It forces clients to guess and makes evolution risky. Good design uses specific types, small composable fields, and stable conventions.

372. What is wrong with a field named user(id: ID, email: String, username: String): User?

Three optional arguments create ambiguous semantics when more than one is supplied, and nothing is required. It also prevents query cost analysis from knowing the access pattern. Prefer separate fields or a @oneOf input that enforces exactly one key.

373. Why is exposing a generic Query.execute(sql: String)-style field a bad idea?

It defeats the type system, invites injection, and makes cost analysis impossible. Clients also lose introspection and tooling support. Any capability that broad belongs behind a dedicated, validated field.

374. How should you model a field that is expensive to compute and rarely needed?

Keep it a separate nullable field so it is resolved only when selected. Add caching or a dataloader behind it, and consider documenting the cost. Never compute it eagerly in the parent resolver.

375. What does the Node interface provide in the Relay spec?

It declares a single non-null id: ID! field, and the schema exposes a node(id: ID!) field to refetch any object by that global ID. This enables generic cache refetching without knowing the type. The ID should encode the type and be opaque to clients.

376. Why should global IDs be opaque rather than raw database primary keys?

Opaque IDs hide implementation details and let you change storage without a client-visible break. They also discourage clients from constructing IDs manually. Base64 of Type:id is a common, simple implementation.

377. What happens if a client sends a syntactically valid but nonexistent cursor?

Behavior should be defined: either return an empty page or a typed error, documented in the schema. Silently ignoring it and returning the first page is the worst option because it causes infinite loops in clients. Prefer a clear error for malformed cursors.

378. How do you paginate a connection whose underlying source cannot do keyset pagination?

You can materialize an ordered snapshot, or emulate keyset by loading and filtering in memory for bounded result sets. Document the limitation and cap the page size to keep it safe. For large datasets, invest in an index that supports the sort order.

379. Why should first and last be bounded by the server?

Unbounded page sizes let a client request enormous result sets and exhaust memory or database connections. Enforce a maximum and either reject or clamp the value. Rejecting is more honest because clamping silently changes semantics.

380. What is a DataLoader and which problem does it solve?

It batches and caches per-request loads, collapsing many individual lookups into a single query. This solves the N+1 problem that appears when resolving a list of objects with nested fields. Each request needs its own loader instance to avoid cross-request cache leakage.

381. Why must a DataLoader instance not be shared across requests?

Its cache is keyed only by ID, so a shared instance could return data loaded under another user's authorization context. It would also grow without bound. Create loaders per request in the context factory.

382. How does query depth limiting protect a GraphQL server?

It rejects documents whose nesting exceeds a threshold, preventing deeply recursive queries from exhausting resources. Depth alone is insufficient for wide queries, so pair it with complexity scoring. Set the limit from the deepest legitimate client query.

383. Why is persisted queries a security and performance win?

Clients send a hash instead of a full document, so arbitrary queries cannot be executed against production. It also reduces request size and enables allowlisting. The trade-off is a build and registration step when queries change.

384. Should a single-object query return null or an error when the object does not exist?

Returning null with no error is usually better, because "not found" is a normal outcome and the client can render an empty state. Reserve errors for cases the client cannot reasonably handle. If absence is exceptional for your domain, use a typed payload instead of a top-level error.

385. Why is Int a poor choice for large identifiers or counters?

GraphQL's Int is a signed 32-bit integer, so values above roughly 2.1 billion overflow and fail serialization. Use ID for identifiers and a Long or BigInt custom scalar for large numbers. This is a common production bug with auto-increment IDs.

386. When would you use ID instead of String for an identifier?

ID signals intent and accepts both strings and integers on input, serializing as a string. It also participates in Relay's global ID convention. Use String when the value is genuinely textual rather than an identifier.

387. What is the difference between parseValue and parseLiteral on a custom scalar?

parseValue handles variables, which arrive as JSON values, while parseLiteral handles inline literals and receives an AST node. Implementing only one breaks either variable-based or literal-based queries. Both must accept the same logical inputs.

388. How strict should a custom scalar's input validation be?

Strict enough to reject malformed values early with a clear message, but tolerant of harmless formatting differences such as surrounding whitespace or case. Overly strict parsing rejects valid client data and causes support tickets. Document the accepted format in the scalar's description.

389. What is the risk of a custom Email scalar that only checks for an at sign?

It gives a false sense of validation while letting invalid addresses into the system. Either implement meaningful validation or keep the field a plain string and validate in the domain layer. Consistency matters more than the specific rule.

390. Why is a URL scalar risky for security?

Not all URLs are safe to fetch; schemes like javascript: or internal hosts enable XSS and SSRF. Validation must restrict schemes and, for server-side fetches, block private address ranges. Schema-level validation alone cannot cover that.

391. How do you deprecate an entire type?

GraphQL only allows @deprecated on fields, arguments, enum values, and input fields, not on object types directly. Deprecate every field on the type and stop referencing it from reachable fields. Then remove the type once nothing references it.

392. Can you deprecate a required argument?

You can apply @deprecated to an argument definition, but clients still must supply it while it is non-null and has no default. Give it a default first so it becomes optional, then deprecate it. Otherwise deprecation is just documentation with no path to removal.

393. What does making a directive repeatable allow?

It permits the same directive to appear multiple times at one location, for example several @tag annotations on a field. Without the repeatable flag, duplicate use is a validation error. Use it when each application carries independent data.

directive @tag(name: String!) repeatable on FIELD_DEFINITION

394. What is the purpose of @defer and @stream?

@defer lets the server send a response in multiple parts, delivering slow fields later, and @stream delivers list items incrementally. Both improve perceived performance for expensive or long lists. They require transport support such as multipart responses, which not all clients or gateways provide.

395. Why can adding a field to an interface be a breaking change?

Every type implementing that interface must now define the field, and third-party or downstream implementers may break. Clients that select the new field on the interface will also request it from all implementations. Add it as nullable and coordinate implementation updates.

396. What are the rules for an object type implementing an interface field?

The implementing field must exist with a compatible type: the same type, a subtype for object return types, or stricter non-null. Arguments must match exactly, including names and types. Violating these rules fails schema validation at build time.

397. Why is changing a field's type from String to a custom scalar breaking?

Clients generate types from the schema, so the change alters their models even if the wire format is identical. It also breaks fragment compatibility checks. Introduce a new field with the new type and deprecate the old one.

398. Why do mutations on the root type execute serially?

The spec requires mutation root fields to be resolved one at a time in document order, because each may depend on the previous write. Queries, by contrast, may resolve fields in parallel. Do not rely on this ordering for nested fields inside a mutation result.

399. What is a good naming convention for mutations?

Use a verb plus noun in camelCase, such as createOrder, cancelOrder, updateShippingAddress. The name should describe the business intent rather than the storage operation. Avoid generic names like update that collide across domains.

400. How would you design a bulk create mutation?

Accept a list of input objects and return per-item results so partial success is representable. Include an index or client key in each result to map outcomes back to inputs. Decide explicitly whether the operation is all-or-nothing and document it.

type BulkCreateResult { index: Int!, id: ID, error: String }

401. What should a delete mutation return?

Return the deleted entity's ID, or the entity itself, so clients can update caches without a refetch. Also return a payload wrapper so errors and correlation fields can be added later. Returning only a Boolean loses information the client usually needs.

402. What is the difference between patch and replace semantics for an update input?

Patch treats absent fields as unchanged, while replace treats absent fields as reset to default or null. Mixing them in one mutation is a frequent source of data loss. Pick one and encode it in the mutation name and documentation.

403. How do you model a "set value or clear value" requirement in an input?

Use a nullable wrapper input like { value: String }, where an absent wrapper means unchanged and { value: null } means clear. This preserves the three-state distinction that a bare nullable field cannot express. Document the pattern because it is not obvious from the type alone.

404. Why is returning ORM entities directly from resolvers risky?

ORM entities carry lazy relations and internal columns that can trigger unexpected queries or leak data. They also couple the schema to the persistence model. Map to dedicated domain or DTO types before returning.

405. When should a connection appear as a nested field rather than on the root query?

When the collection is scoped to a parent, such as a user's orders, nesting expresses ownership and lets authorization apply at the parent. Root connections are for global collections. Duplicating both is fine if the semantics and permissions differ.

406. What happens to cursors when the sort order changes between requests?

Cursors encoded for one ordering are meaningless in another, so the client can get duplicates or omissions. Cursors should encode the sort they belong to, and the server should reject or ignore mismatched ones. Document that cursors are only valid for the same arguments.

407. How do filters interact with cursor stability?

A cursor is only valid for the same filter set; changing filters changes the result window entirely. Encode enough of the query shape in the cursor, or require clients to restart pagination when filters change. Many APIs fail explicitly on a cursor and filter mismatch.

408. Why is sorting by an unindexed field a performance problem in paginated queries?

Each page requires a full sort of the filtered set, so cost grows with dataset size and repeats on every request. Keyset pagination cannot use an index it does not have. Either add a supporting index or restrict sortable fields in the enum.

409. How do you paginate search results ordered by relevance?

Use the search engine's stable sort token, often a score plus a tiebreaker, as the cursor. Avoid re-ranking between requests, which invalidates cursors. If scoring is unstable, prefer a snapshot or a time-boxed search session.

410. What is the risk of a connection that returns totalCount but no pageInfo?

Clients can compute page numbers but cannot reliably know whether more data exists, so they may issue extra empty requests. It also encourages offset thinking in a cursor API. Provide pageInfo as the primary signal and treat counts as supplementary.

411. Why should a fragment's name be unique within a document?

Fragment names are document-scoped identifiers, so duplicates are a validation error. Unique names also let tooling deduplicate and cache fragments. Prefixing with the component or domain name avoids collisions when composing.

412. What happens with an unused fragment in a document?

It is a validation error, because every defined fragment must be used by the operation. This catches dead code left after refactoring. Client build tooling usually strips unused fragments automatically.

413. Why are cyclic fragment spreads rejected?

A fragment that spreads itself, directly or indirectly, would create infinite recursion during validation and execution. The spec forbids fragment cycles outright. Break the cycle by extracting a smaller fragment or using a bounded depth.

414. Can you apply @skip to a named fragment spread?

Yes, and it skips the entire spread including all its fields, which is more efficient than skipping fields individually. It is often used to conditionally include an expensive section. The same directive also applies to inline fragments.

415. Why must a fragment spread's type condition match the parent type's possible types?

Otherwise the selection could never apply, which is almost certainly a bug in the query. Validation rejects spreads whose type condition cannot intersect the parent. Inside a union or interface, member-type spreads are valid because runtime types may match.

416. What is the benefit of putting fragments on interfaces?

A fragment on an interface can be spread anywhere that interface appears and selects only shared fields, so it works across all implementations. Components that only need common fields stay decoupled from concrete types. Type-specific needs still require inline fragments.

417. Why do aliases matter when the same field is requested with different arguments?

Without aliases, two selections of the same field with different arguments conflict during field merging validation. An alias gives each result a distinct key in the response. This is essential for fetching the same resource at two scales.

small: avatar(size: 32)
large: avatar(size: 256)

418. What are GraphQL's field merging rules?

Selections on the same response key must have compatible types and identical arguments, otherwise validation fails. Fields with different arguments must use aliases. The rules apply across fragment spreads too, which is why conflicting fragments are caught at build time.

419. Why should operations be given explicit names?

Named operations appear in server logs, traces, and persisted query registries, making production debugging far easier. Anonymous operations are harder to attribute to a client code path. A consistent naming scheme also supports allowlisting.

420. Can a document contain multiple operations, and what does the client send?

Yes, but the request must specify which operation to run via operationName, unless the document has exactly one operation. Clients typically send one operation per request. Multiple operations are mainly useful for documentation and tooling.

421. Why might a team disable introspection in production?

It reduces schema discovery for attackers and shrinks the attack surface slightly. The cost is degraded tooling and harder debugging, and determined attackers can still infer the schema. Many teams keep it enabled behind authentication instead.

422. What is the difference between hiding a field and removing it for authorization?

A schema is usually shared by all clients, so per-user schema filtering complicates caching and tooling. Hiding fields per user also leaks structure through inconsistent introspection. Prefer returning an authorization error or null from the resolver, and keep the schema uniform.

423. Why is it a problem to return a non-null list and then filter out unauthorized items?

Filtering silently changes the count and can break pagination offsets or cursors. It also makes totalCount inconsistent with the visible edges. Either apply authorization in the query so pagination reflects it, or document the filtering behavior.

424. How do you expose computed or derived fields without storing them?

Add a resolver that derives the value from existing fields or a service call, and keep it out of the input type. Cache or batch the computation if it is expensive. Document that it is derived so clients do not expect to write it.

425. What is a sensible way to version a GraphQL API?

Most teams avoid URL or header versioning and evolve the single schema continuously with deprecation and additive changes. When a true break is unavoidable, run a new endpoint or a namespaced root field for a transition period. Versioning per field quickly becomes unmanageable.

426. Why is a schema registry useful in a microservice GraphQL setup?

It stores the composed schema and validates that each service's changes remain compatible with the whole graph. It also enables composition checks in CI and a history of deployments. Without it, two teams can independently introduce conflicting changes.

427. Should an errors list in a mutation payload be non-null?

Yes, [UserError!]! is the common choice: an empty list means success, so clients never null-check the collection. Each element being non-null guarantees a uniform shape. The alternative, a nullable list, forces clients to handle three states for no benefit.

428. How should a payload error identify which input field failed?

Include a field or path value that maps back to the input, for example ["input", "email"] or a dotted string. This lets clients attach messages to the right form control without parsing the message text. Keep the path format documented and stable.

type UserError { message: String!, field: [String!], code: ErrorCode! }

429. Should an error code be an enum or a plain string?

An enum is validated and introspectable, so generated clients get typed switches. A string is easier to extend without a schema change and tolerates unknown values. For a closed, well-understood set of domain errors an enum is usually better.

430. Where should user-facing error messages be localized?

Return a stable machine code and let the client translate, optionally supplying a default message as a fallback. Translating on the server requires knowing the request locale and creates cache fragmentation. This also keeps the API free of presentation concerns.

431. How can a mutation detect a conflicting concurrent update?

Include a version or updatedAt field in the input and compare it to the stored value inside the transaction. If it does not match, reject the write with a typed conflict error. This is optimistic locking expressed through the API contract.

432. How should a version conflict be signalled to the client?

Return a typed payload result carrying the current server state so the client can merge or prompt the user. Use a transport error only if the client genuinely cannot proceed. A conflict is an expected outcome, not an infrastructure failure.

433. Why is an optional argument that changes the return type problematic?

GraphQL field types are static, so one field cannot return different shapes based on arguments without a union or interface. Clients then cannot know statically what they will get. Use separate fields or an abstract return type with inline fragments.

434. How do you model search results that span several entity types?

Return a list of a union or a common SearchResult interface, with each member carrying its own fields. Clients branch with inline fragments on __typename. Include a relevance or rank field at the union level if the interface can express it.

435. How do you design a heterogeneous activity feed?

Use a union of activity types with shared fields pushed into an interface such as id, actor, and createdAt. The connection edges then paginate the union. Clients render per type via inline fragments with a default fallback for unknown types.

436. What is the cost of nesting multiple paginated fields in one query?

Each connection can multiply the number of database queries and returned rows, so cost grows multiplicatively with nesting. This is a common way to overload a server without exceeding a simple depth limit. Complexity scoring should weight connections by their requested page size.

437. Why expose a node(id: ID!) field alongside per-type root fields?

It gives clients a uniform way to refetch any object when only its global ID is known, which is essential for cache normalization. Per-type fields remain useful for lookups by other keys. Both can coexist as long as IDs are global and stable.

438. Should arguments carry descriptions as well as fields?

Yes, especially for arguments whose meaning is not obvious, such as a timezone, a unit, or a default behavior. Argument descriptions appear in introspection and IDE tooltips. They are the cheapest place to prevent misuse.

439. What makes a deprecation reason useful?

It names the replacement and ideally a timeline, such as "Use displayName; removed after 2025-01". Vague reasons like "deprecated" give clients no migration path. The reason shows up in tooling warnings, so it is read by developers at the moment they need it.

440. Should enum values have descriptions?

Yes when the value name is not self-explanatory or when it maps to a business rule, such as what PENDING_REVIEW implies. Descriptions are introspectable and appear in generated docs. They also help prevent clients from guessing semantics.

441. How can internal-only fields be kept out of a public schema?

Build the public schema from a filtered subset, for example by marking fields with a custom directive and stripping them at build time. Do not rely on resolvers returning null, because the field is still discoverable and queryable. Keep one source of truth and derive both schemas from it.

442. Why is an object type with no fields invalid?

The spec requires object, interface, and input object types to define at least one field. An empty type cannot be queried meaningfully and would fail schema validation. Model a marker concept as an enum or add a meaningful field such as id.

443. What are the reserved root operation type names?

Query, Mutation, and Subscription are the default names, and a schema may rename them only through explicit schema definition syntax. Most teams keep the defaults for tooling compatibility. You cannot name a regular object type Query and expect it to serve as the root.

444. Why must a subscription root field return a single type rather than a list?

Each event produces one payload delivered as a separate message, so the field represents a stream of single values. Returning a list would conflate one event with many. For batching, emit one event per change instead.

445. Can a mutation be executed as a query?

No, root fields are bound to their operation type, so a mutation field cannot appear in a query document. This separation is deliberate, since queries are expected to be side-effect free and safely cacheable. Attempting it is a validation error.

446. Why are Boolean flag arguments a schema smell?

A flag like includeDeleted: Boolean hides a mode switch that usually grows into more modes, and each new flag multiplies combinations. An enum expresses the intent explicitly and extends cleanly. Flags also read poorly at call sites.

447. How should soft delete be represented in a schema?

Keep the entity type and expose a deletedAt timestamp, with an explicit filter or argument for including deleted records. Do not silently hide records from lists while node still returns them, because that inconsistency confuses clients. Decide whether deleted items are visible on direct lookup and document it.

448. Rewrite getUserData(id: ID!, flags: String): JSON into a well-typed design.

Replace it with a typed user(id: ID!): User whose type declares each field explicitly, and move optional behaviors into enums or separate fields. Returning JSON prevents introspection and validation, and a stringly typed flags argument is impossible to validate. The typed version also lets clients select only what they need.

user(id: ID!): User
type User { id: ID!, displayName: String!, status: UserStatus! }

449. Why is it useful to separate a mutation's input type from the entity's output type?

Inputs should expose only writable fields, while outputs include server-generated values such as IDs, timestamps, and computed status. Reusing one type for both leaks writable paths for immutable fields and invites clients to send them. Separate types also let each evolve independently.

450. How do you keep a schema consistent when many teams contribute fields?

Enforce conventions through linting rules and schema review, covering naming, nullability defaults, and required descriptions. Automate breaking-change checks in CI so review focuses on design rather than mechanics. A shared style guide prevents a schema that reads like several unrelated APIs.

451. Why does a query that returns a list of posts and their authors often produce N+1 database queries?

GraphQL executes field by field, so the root resolver loads the list once and then the author resolver runs once for every post in that list. Each of those calls is an independent database round trip, giving one query for the list plus N queries for the authors. The server has no idea at the resolver level that all of those lookups could have been a single query.

posts()            -> SELECT * FROM posts              (1)
post.author() x N  -> SELECT * FROM users WHERE id = ? (N)

452. How does DataLoader solve the N+1 problem?

DataLoader collects the keys requested during one tick of the event loop and dispatches them to a single batch function. That function returns an array of values positionally aligned with the keys, so N individual lookups become one query. Because GraphQL resolves sibling fields concurrently in the same execution step, the keys arrive close enough together to be coalesced.

453. Why must a DataLoader instance be created per request instead of at module scope?

A module-scope loader would share its cache and pending batches across all users, which leaks data between requests. It would also grow without bound and could return stale rows long after they changed. Create loaders inside the request context so the cache lifetime matches the request lifetime.

context: ({ req }) => ({
  loaders: { userById: new DataLoader(batchUsers) }
})

454. What does the per-request cache in DataLoader actually cache, and what are its semantics?

It is a map from cache key to the resolved promise of the value, scoped to that loader instance. Caching the promise means concurrent duplicates share one in-flight call, and repeated keys within the request never hit the batch function twice. The cache is only as correct as the key: if the key omits tenant or locale, you will serve the wrong row.

455. How does DataLoader batch keys by cache key, and what happens when the same key is requested twice in one tick?

DataLoader deduplicates keys before calling the batch function, so a repeated key appears only once in the keys array. Both callers receive the same promise and therefore the same value. This is why duplicate keys never reach your SQL WHERE id IN (...) list.

456. Does DataLoader guarantee that returned results are in the order of the requested keys?

Yes, the contract is positional: result at index i corresponds to key at index i, and DataLoader does not sort or match by id. Your batch function is responsible for mapping database rows back to the input key order, typically with a lookup map built from the rows. Returning rows in arbitrary database order is the most common cause of fields showing the wrong object.

const byId = new Map(rows.map(r => [r.id, r]));
return keys.map(k => byId.get(k) ?? null);

457. What happens if a batch function returns results out of order or with the wrong length?

DataLoader throws a runtime error complaining that the data loader did not return an array of the same length as the keys, or silently assigns values to the wrong keys when the length happens to match. Both outcomes are serious: the first fails the request, the second corrupts data. Always assert length equality and map by key rather than relying on query order.

458. Why should a batch load function handle duplicate keys?

Although DataLoader deduplicates within one batch, keys can repeat across separate batches and custom cache maps can be disabled. A batch function that assumes uniqueness may return fewer rows than keys and break the positional contract. Building a map keyed by id makes duplicates harmless.

459. How do you implement DataLoader in HotChocolate?

HotChocolate has first-class DataLoader support: you annotate a resolver parameter with [DataLoader] or inject a class deriving from BatchDataLoader, and register it as a scoped service. The execution engine collects the keys from all resolving fields in the same batch and calls your LoadBatchAsync method once. Registration must be scoped, because the loader's cache must not outlive the request.

public class UserByIdDataLoader : BatchDataLoader<int, User>
{
    protected override Task<IReadOnlyDictionary<int, User>> LoadBatchAsync(
        IReadOnlyList<int> keys, CancellationToken ct) => _db.Users
        .Where(u => keys.Contains(u.Id))
        .ToDictionaryAsync(u => u.Id, ct);
}

460. What is the difference between request-scoped caching and a distributed cache such as Redis?

A request-scoped cache lives in memory for a single execution and needs no invalidation because it disappears with the request. A distributed cache is shared across instances and requests, so it survives restarts but introduces invalidation, serialization and consistency problems. Request-scoped caching removes duplicate work; distributed caching removes repeated work across users.

461. When is a distributed cache the wrong tool for the N+1 problem?

It is the wrong tool when the data is user-specific, changes often, or the win would come from batching rather than storage. Putting per-user rows in Redis adds a network hop and a correctness risk without removing the number of round trips. Batch and join first, then cache only the genuinely hot, shared data.

462. How can you detect N+1 queries in a running GraphQL service?

Count database queries per GraphQL operation and compare it with the number of fields resolved; a count that scales with list length is the signature. Trace spans per resolver and look for repeated identical SQL statements within one request. In development, ORM query logs and per-field instrumentation make the pattern obvious quickly.

463. How does DataLoader batching differ from a database join, and when do you prefer each?

A join fetches parent and child rows in one database round trip, while batching fetches children in one query after the parents are known. Joins are better for tightly coupled data on the same database and for large result sets where the round trip dominates. Batching is better when children come from another service, when the parent set is small, or when you cannot change the query shape.

464. How does field-level caching differ from caching a whole response?

Field-level caching stores the value of one resolved field keyed by its arguments and parent, so it can be reused by many different operations. Response caching stores the serialized output of an entire operation keyed by document and variables, so it can only serve identical requests. Field-level caching has a finer hit rate but requires careful key design; response caching is simpler but coarser.

465. Why is HTTP caching for GraphQL harder than for REST?

In REST the URL identifies the resource, but in GraphQL a single endpoint serves every operation, so the URL says nothing about the response. The cache key has to include the query document and the variables, which standard HTTP caches do not understand. That is why GraphQL caching usually relies on GET with encoded parameters, explicit cache headers, or a CDN rule that keys on more than the path.

466. Why are GraphQL queries usually sent as POST, and what does that mean for HTTP caching?

POST is the convention because queries can be large and may contain characters that are awkward in a URL, and because mutations must not be cacheable. The consequence is that browsers, proxies and CDNs treat the request as non-cacheable by default. Only queries sent as GET can participate in normal HTTP caching.

467. How can you make a GraphQL response cacheable by a CDN using GET requests?

Send read-only operations as GET with the document and variables encoded in the query string, then return explicit Cache-Control headers and a Vary header where relevant. The CDN must key on the full query string, not just the path. Mutations stay on POST and are never cached.

GET /graphql?query=query%20Q($id:ID!){user(id:$id){name}}&variables={"id":"1"}

468. What role do ETag and If-None-Match play in GraphQL HTTP caching?

The server hashes the response body, returns it as an ETag, and a client that already has that version sends If-None-Match on the next request. If the hash still matches, the server replies 304 Not Modified with no body, which saves bandwidth but not necessarily server execution. To save execution you need to compute the ETag from cached data rather than by running the operation again.

469. Why do authenticated GraphQL responses usually break CDN caching?

A shared cache cannot store a response that differs per user unless the cache key includes the user, and putting the identity in the key effectively destroys the hit rate. Worse, if the cache ignores the Authorization header, one user's data can be served to another. The usual answer is to cache only public data at the edge and keep personalized fields out of the cached document.

470. What does a Cache-Control header on a GraphQL response do, and who honors it?

It tells browsers, proxies and CDNs how long the response may be reused and whether it is private or shared. private allows only the browser to store it, while public allows shared caches and therefore requires the content to be identical for every user. The server itself does not cache anything just by emitting the header.

471. How do @cacheControl hints work and what are the two common scopes?

You annotate types or fields with a maxAge and a scope so the server can compute the cacheability of the whole response from the fields actually selected. The two scopes are PUBLIC for data that is the same for everyone and PRIVATE for data tied to the viewer. The overall response age becomes the minimum of the selected fields, and any private field forces the whole response to be private.

type Post @cacheControl(maxAge: 60, scope: PUBLIC) {
  title: String
  draft: Boolean @cacheControl(maxAge: 0, scope: PRIVATE)
}

472. What is a persisted query?

A persisted query replaces the full query document with a short identifier that the server looks up from a trusted store. The client sends only the hash or id plus variables, which shrinks requests and means arbitrary documents can be rejected. Many servers support a pre-registered list built at deploy time.

473. How do automatic persisted queries differ from a build-time persisted query list?

With APQ the client first sends a hash, the server replies "not found", and the client then sends the full document which the server stores for later. That is convenient and needs no build step, but the server still accepts arbitrary documents, so it is a performance optimisation rather than a security control. A build-time list only permits documents registered during the release, which blocks unknown operations entirely.

474. Why do persisted queries improve security as well as performance?

If the server only executes documents from a pre-approved list, an attacker cannot send new query shapes to probe fields or bypass a query-cost estimate. The attack surface shrinks to the operations you reviewed and shipped. APQ alone does not give this benefit unless the server is configured to reject unregistered documents.

475. What is query allowlisting and how does it relate to persisted queries?

Allowlisting means the server has an approved set of operations and rejects anything else, usually by hash. Persisted queries are the mechanism that makes allowlisting practical, because the client never needs to transmit a full document. The important detail is the enforcement mode: log-only mode gives you data, strict mode gives you protection.

476. How would you limit response size in a GraphQL API?

Enforce pagination on list fields, cap page sizes at the schema level, and reject operations whose estimated cost exceeds a budget. You can also measure the serialized result and abort once it passes a hard byte limit. Truncating silently is a bad idea, so return a clear error instead.

477. What is query depth limiting and what attack does it prevent?

Depth limiting rejects operations whose nesting exceeds a configured maximum, which stops recursive schemas from being walked arbitrarily deep. The classic example is a friends { friends { friends ... } } cycle that multiplies database work exponentially. It is cheap to compute before execution because it only inspects the document structure.

478. Why is depth limiting alone insufficient protection?

Depth says nothing about breadth, so a shallow query with many aliased list fields can still be enormously expensive. It also ignores the cost of individual fields, where one cheap-looking field might trigger an aggregation over millions of rows. Use depth limits as a coarse guard and cost analysis as the real control.

479. What is query complexity or cost analysis?

Cost analysis walks the parsed document and assigns a weight to each field, multiplying by the expected number of list items and summing across aliases. The server compares the total against a per-user or per-operation budget and rejects overruns before execution. Weights can come from static hints, from pagination arguments, or from measured production costs.

total = sum(fieldCost * estimatedListSize)  // reject if total > budget

480. How can aliases be used to amplify a query, and how do you defend against it?

An attacker repeats the same expensive field under many different aliases, so a small document triggers dozens of identical heavy resolvers. Defending requires counting aliases as separate work in the cost calculation rather than deduplicating by field name. Limiting total alias count per operation is a useful secondary guard.

{ a: expensiveSearch(q:"x") b: expensiveSearch(q:"x") c: expensiveSearch(q:"x") }

481. Why must complexity analysis count aliases rather than just fields in the document?

Because two aliased selections of the same field execute as two separate resolver invocations with separate results. A cost model that deduplicates by field name will systematically underestimate the real work. The same logic applies to fragments spread multiple times.

482. Should introspection be enabled in production? Argue both sides.

Enabling it helps internal tooling, schema explorers and client codegen against production, and it makes debugging easier. Disabling it hides the schema from casual attackers and is often required by security policy, though determined attackers can still infer fields from error messages or field suggestions. For public APIs the common middle ground is to disable it for anonymous traffic and allow it for authenticated internal clients.

483. How do you disable introspection while keeping tooling usable?

Block the introspection fields __schema and __type for untrusted clients based on context, such as an authenticated internal role or an allowlisted network range. Keep it available in staging so client codegen and dashboards still work. Also disable field suggestions in validation errors, otherwise the schema leaks through error messages.

484. What is field-level authorization and where is it usually enforced?

It means deciding access per field or per type rather than per endpoint, because one GraphQL request can touch data with different sensitivity levels. It is usually enforced in resolver-level checks, schema directives that wrap resolvers, or a policy layer the resolvers call. Enforcement must happen on the server for every path that can reach the field, including nested and aliased selections.

485. Compare directive-based authorization with checks inside resolvers.

Directives such as @auth(requires: ADMIN) keep the policy visible in the schema and applied consistently by a middleware, which is easier to audit. Resolver checks are more flexible and can use data already loaded, but they are easy to forget on a new field. Many teams use directives for coarse rules and resolver checks for decisions that depend on the loaded object.

type Query { revenue: Money @auth(requires: FINANCE) }

486. Why is authorization in the GraphQL layer alone often not enough?

If the resolver fetches the whole row and then hides fields, the data still left the database and may appear in logs, caches or error payloads. Nested fields and future operations can reach the same data through another path. Defense in depth means also restricting at the data layer, for example with row-level security or scoped queries.

487. How does authentication context propagate from the HTTP request to resolvers?

The server extracts and verifies the token once when the request arrives, then puts the resulting identity into the per-request context object that every resolver receives. Resolvers read the identity from that context rather than re-parsing headers. For subscriptions the identity is established during the connection handshake and carried on each operation.

context: async ({ req }) => ({ user: await verify(req.headers.authorization) })

488. What are the risks of trusting a userId sent in GraphQL variables?

Variables are client-controlled, so a userId argument is an authorization request, not a fact. If a resolver uses it to decide whose data to return without comparing it with the authenticated identity, any user can read another user's records. The rule is: derive identity from the token, treat arguments only as a subject to be authorized.

489. How do you rate limit GraphQL per user?

Key the limiter on the authenticated user id from the verified token, with a fallback to IP for anonymous traffic, and store counters in a shared store such as Redis. Because one HTTP request can contain many operations and one operation many fields, the limit should apply to cost rather than raw request count. Also keep a separate, tighter limit for mutations and expensive operations.

490. Why is rate limiting per HTTP request misleading for GraphQL?

A single request can contain a fan-out query worth hundreds of REST calls, so counting requests says little about load. Two clients with the same request rate can impose wildly different cost. Cost-based limits map much closer to actual resource consumption.

491. How do you implement per-operation rate limiting using query cost?

Compute the cost of the operation after parsing and validation, then charge that amount against the user's bucket before execution. Persisted queries help here because the cost can be precomputed and cached per document hash. Reject with a clear error and a retry hint when the bucket is empty.

492. How do timeouts and cancellation work in a GraphQL server?

The HTTP layer gives you a cancellation token that is cancelled when the client disconnects or the deadline passes, and the execution engine passes it to resolvers. Resolvers must accept the token and forward it to database and HTTP clients, otherwise the work continues after the request is gone. A server-side execution deadline is safer than relying on the client to give up.

493. Why is it important to propagate cancellation to downstream calls?

Without propagation the server keeps holding a connection and consuming database time for a response nobody will read. Under load this turns abandoned requests into a denial of service against your own database. Passing the token through every layer lets the driver abort the query or HTTP call as soon as the client goes away.

// wrong
var user = await db.Users.FindAsync(id);
// right
var user = await db.Users.FindAsync(id, ct);

494. What is the difference between graphql-ws and the legacy subscriptions-transport-ws protocol?

They are two different WebSocket subprotocols that share a confusingly similar name. The modern graphql-ws library speaks the graphql-transport-ws subprotocol, while the deprecated subscriptions-transport-ws library negotiated the graphql-ws subprotocol. Mixing a client and server from opposite families results in the connection closing during the handshake.

graphql-ws:      subprotocol "graphql-transport-ws"
legacy library:  subprotocol "graphql-ws"

495. Why is graphql-ws preferred for new projects?

It is actively maintained, has a smaller and clearer protocol with explicit complete messages, and supports connection initialisation with proper error reporting. The legacy protocol is deprecated and has known issues around acknowledgements and message semantics. Most server and client libraries now implement graphql-ws, so interoperability is rarely a reason to stay on the old one.

496. How does a subscription differ from a query in terms of transport lifetime?

A query is a single request-response exchange that ends when the response is written. A subscription registers a long-lived operation that stays open and pushes a new result each time a matching event occurs. That means the server must track the operation for the whole connection lifetime, not just for one execution.

497. How does the server know which clients to notify when a subscription event occurs?

When the subscription starts, the server registers the parsed operation and its variables against a topic derived from the field and its arguments. Each incoming event is matched against those registrations, and the matched operations are re-executed with the event payload as the root value. Unsubscribing removes the registration so no further executions happen.

498. What is the role of a pub/sub system in GraphQL subscriptions?

The pub/sub layer decouples the code that produces an event from the connection that must receive it. The mutation publishes to a topic and the subscription runtime subscribes to that topic on behalf of each client. It also provides the seam where you swap in-memory delivery for a broker when you scale out.

499. Compare in-memory pub/sub, Redis pub/sub and Kafka for subscriptions.

In-memory is the simplest and fastest but only reaches clients connected to the same process. Redis pub/sub fans out across instances with very low latency but has no durability, so a message published while an instance is down is lost. Kafka adds durability, replay and consumer groups at the cost of higher latency and operational weight.

500. Why does in-memory pub/sub break when you run more than one instance?

An event published in instance A is only delivered to subscribers registered inside instance A. If the client's WebSocket landed on instance B, it never sees the event, so delivery depends on load balancer luck. This produces the classic symptom of notifications working locally and intermittently in production.

501. How do you scale subscriptions across multiple instances?

Publish events to a shared broker and have every instance subscribe and deliver to its own local clients, using a topic per subscription channel. Sticky sessions are not required with this approach, because any instance can handle any event. The broker becomes a critical dependency, so you need connection retry and a fallback path.

502. What are the trade-offs of using Redis pub/sub versus a Kafka topic for fan-out?

Redis gives sub-millisecond delivery and trivial setup, which suits ephemeral notifications where a missed message is acceptable. Kafka persists events and lets a restarted consumer catch up, which matters for order-sensitive or auditable streams. Kafka also lets you partition by key so events for one entity stay ordered, while Redis channels give no ordering guarantee across publishers.

503. What is backpressure in the context of subscriptions?

Backpressure is what happens when events arrive faster than a client can consume them. The server buffers pending messages, and if it keeps buffering the process grows until it runs out of memory. The fix is a bounded queue per connection with a policy: drop, coalesce, or disconnect the slow consumer.

504. How do you protect a server from slow consumers?

Give each connection a bounded outbound buffer and close the connection when it fills, rather than buffering without limit. You can also coalesce events by key so only the latest state is sent, which suits dashboards showing current values. Track per-connection queue depth as a metric so you notice the problem before memory does.

505. What is a live query and how does it differ from a subscription?

A live query re-runs the original operation whenever the data it touched changes, so the client receives a new full result without defining an event stream. A subscription requires the client to define the exact event shape and usually a dedicated publish path. Live queries are easier for clients but much harder for servers because they need dependency tracking.

506. When would you choose live queries over subscriptions?

Live queries fit dashboards and detail views where the client just wants the current value of a normal query to stay fresh. Subscriptions fit event semantics, such as chat messages or job progress, where each event matters individually. If your server cannot track data dependencies reliably, subscriptions with explicit invalidation are the safer choice.

507. What problem do @defer and @stream solve?

They let the server send part of a response immediately and deliver the rest later, which improves perceived latency for slow fields. @defer postpones a fragment, while @stream sends list items incrementally. The client can render the fast parts while the expensive parts are still resolving.

query { product(id: "1") { name ... @defer { reviews { body } } } }

508. How does @defer change what the client receives over time?

The first payload contains the non-deferred data plus a marker that a deferred fragment is still pending. Later payloads contain the fragment data and an incremental path identifying where it belongs. The client merges each payload into the existing result, so it must handle a null value that later fills in.

509. What are the transport requirements for @defer and @stream?

The transport must support multiple payloads for one operation, so plain JSON over a single response body is not enough. Servers typically use multipart/mixed or a WebSocket connection, and the client must understand the incremental delivery format. HTTP/1.1 without chunked transfer makes this awkward, and some proxies buffer the whole response and defeat the purpose.

510. How does a client handle a deferred response that arrives in multiple parts?

The client applies each incremental payload to the cached result at the path indicated, then notifies subscribers of the update as data arrives. It must also handle an error in a deferred part without discarding the data already shown. Cache writes in this mode should be treated as merges, not replacements.

511. What does the GraphQL multipart request spec define for file uploads?

It defines a multipart/form-data request where one part carries the JSON body of the operation with null placeholders, and other parts carry the files referenced by those placeholders. The server maps each file part back into the variables before execution. This lets a mutation take an Upload scalar as an argument.

512. Why is the single-request multipart upload approach often discouraged in production?

Files stream through the GraphQL server, which then usually forwards them to object storage, doubling bandwidth and tying up application workers. It also bypasses the storage layer's own protections and makes resumable uploads hard. Proxies and gateways often impose body size limits that break large uploads.

513. How do presigned URLs solve the file upload problem in GraphQL?

The server issues a short-lived signed URL and the client uploads the bytes directly to object storage, so the GraphQL server never handles the payload. A follow-up mutation then attaches the resulting object key to a domain entity. This keeps the API stateless and lets storage handle size limits, retries and multipart uploads.

514. What is the flow for uploading a file via presigned URL and then attaching it in a mutation?

The client requests an upload target through a mutation, uploads the file to the returned URL, then calls a second mutation with the object key and metadata. The server verifies that the key belongs to the caller before attaching it, because the key is client-supplied. Orphaned uploads are cleaned up by a lifecycle rule on the bucket.

requestUpload(contentType) -> { url, key }
PUT url (bytes)
attachFile(key, postId)    -> Post

515. How do you validate an uploaded file without loading it fully into memory?

Inspect the declared content type and the first bytes for magic numbers while streaming, and enforce a maximum size with a counting stream that aborts on overrun. Deeper checks such as image decoding or virus scanning should run asynchronously after the upload. Never trust the filename or the client-provided content type on their own.

516. Can you send multiple operations in one HTTP request, and what are the trade-offs?

Yes, by sending an array of operation objects, which some servers call request batching. It reduces round trips for clients but makes per-request rate limiting, tracing and caching ambiguous, and one expensive member can fail the whole batch. For that reason many teams disable it or limit it to a small number of small operations.

[{ "query": "{ me { id } }" }, { "query": "{ cart { total } }" }]

517. What is request batching on the client and what risk does it introduce on the server?

The client library collects operations issued in a short window and sends them together to reduce network overhead. On the server, batching makes a single HTTP request represent arbitrary amounts of work, which undermines request-count rate limits and cost estimates. It also complicates partial failure handling and makes traces harder to attribute.

518. How does HTTP compression interact with GraphQL responses?

GraphQL responses repeat field names in every object, so gzip or brotli compresses them very effectively, often by more than ten to one. Compression should be negotiated with Accept-Encoding and skipped for small bodies where the header overhead dominates. It also reduces the benefit of persisted queries, since the document is a small part of a compressed payload.

519. When should you avoid compressing a GraphQL response?

When the body is small enough that compression costs more CPU than it saves, which is common for tiny mutations. Avoid it for already-compressed binary payloads carried in base64, since they will not shrink. Also be careful with compression plus secrets in responses, because compression side channels can leak information.

520. Why does connection reuse matter for GraphQL performance?

Every new TCP and TLS handshake costs latency before the first byte of the query is even sent, and GraphQL clients often issue many small operations. Reusing connections amortises that cost and avoids exhausting ephemeral ports under load. On the server side, connection pooling to the database matters for the same reason.

521. What is the difference between a keep-alive agent and HTTP/2 multiplexing?

Keep-alive reuses a single connection for sequential requests, avoiding repeated handshakes but still serialising requests on that socket. HTTP/2 multiplexes many concurrent streams over one connection, so parallel operations no longer need parallel sockets. HTTP/2 also compresses headers, which helps when clients send authenticated requests frequently.

522. How can resolver-level caching be implemented safely?

Cache the resolved value under a key built from the field, its arguments, the parent identity and every context input that affects the result. Keep the entry lifetime short and tied to the request when the data is user-specific. Prefer wrapping the data-access function rather than the resolver, so the cache key is derived from explicit inputs.

key = `${parent.id}:${args.locale}:${ctx.tenantId}`

523. What is the risk of caching resolver results without including variables and context in the key?

Two callers with different arguments or different tenants collide on the same entry, so one user receives another user's data. This is a correctness and security bug, not just a stale-data bug, and it is easy to miss in tests that only use one user. Include every input that can change the output, and prefer omitting the cache when you are unsure.

524. How do you cache a resolver that depends on the current user?

Either include the user id in the key, which gives a correct but low hit rate, or split the work so the user-independent part is cached and the permission check runs per request. A common pattern is to cache the raw entity by id and apply field-level authorization afterwards. Never cache the post-authorization projection unless the key includes the authorization decision.

525. Why is caching a nullable field result different from caching an object?

A null may mean "does not exist" or "not authorized to see", and caching the second case can lock a user out after their permissions change. Null results also often come from transient failures, which should not be memoised. Cache positive results by default and treat null as uncacheable unless you know the reason.

526. How do you optimize database access for deeply nested selections?

Look at the selection set before hitting the database and fetch only the columns and relations that were actually requested. Batch child lookups with DataLoader or use a join for one-to-many relations that are always needed together. Add indexes for the filter and sort combinations that the nested resolvers generate.

527. What is the risk of letting clients drive joins through nested selections?

A nested selection can translate into a join tree whose size and cost the schema author never anticipated. Each additional level multiplies rows, so a modest-looking document can produce a query the database cannot plan well. Bound the shape with pagination, depth limits and explicit cost weights on expensive relations.

528. How do projection strategies avoid over-fetching from the database?

Projection maps the GraphQL selection set onto the database column and relation list, so the generated SQL selects only the requested fields. This avoids the classic ORM behaviour of loading every column for the entity. It also makes lazy-loading bugs visible, because an unprojected field triggers an extra query.

529. What does populate mean in an ORM context and why is it dangerous with GraphQL?

Populate tells the ORM to eagerly load relations, often recursively and often without knowing which relations the client asked for. Under GraphQL this either over-fetches huge graphs or under-fetches and causes N+1 queries. It becomes dangerous when the populate depth is derived from client input, since a deep selection can explode into a massive join.

530. How does HotChocolate's projection middleware use selection sets?

The middleware inspects the selection set for a field and rewrites the IQueryable so only the selected columns and includes are materialised. That means the resolver can return a queryable and the engine decides the final SQL shape. It requires care with computed or authorization-dependent fields, which may need explicit handling.

[UseProjection]
public IQueryable<Book> GetBooks([Service] AppDb db) => db.Books;

531. How do filtering, sorting and pagination middleware change resolver design?

The middleware applies client arguments to the IQueryable before execution, so the resolver returns a base query and does not hand-code every filter combination. This keeps the SQL correct and lets the database use indexes, but it also means clients can generate query shapes you never tested. Restrict which fields are filterable and sortable to those with supporting indexes.

532. What is cursor-based pagination and why is it preferred over offset pagination in GraphQL?

A cursor encodes a position in a stable ordering, so the next page starts after a known row rather than at a numeric offset. Inserts and deletes do not shift the window, which avoids duplicated or skipped items. It also performs better on large tables because the database can seek instead of counting rows.

533. What does the Relay connection specification define?

It defines a shape with edges, each having a node and a cursor, plus pageInfo with hasNextPage, hasPreviousPage, startCursor and endCursor. It also defines the first, after, last and before arguments. Following it gives client libraries pagination behaviour they already understand.

posts(first: 10, after: "c3") { edges { node { id } cursor } pageInfo { hasNextPage endCursor } }

534. How do you prevent a client from requesting an unbounded page size?

Clamp first and last to a schema maximum, and reject values above it rather than silently truncating. Enforce a default when the argument is omitted so a bare list field cannot scan a table. Apply the clamp in shared middleware so every connection field inherits it.

535. What is field middleware in GraphQL.NET and what is it used for?

Field middleware wraps resolver execution, letting you run code before and after the resolver for a specific field or globally. Typical uses are timing, logging, authorization, caching and exception translation. Because it composes around the resolver, it can short-circuit execution or transform the returned value.

Field<string>("name").Resolve(ctx => ...).AddMiddleware<TimingMiddleware>();

536. How would you implement timing for each field using GraphQL.NET middleware?

Record a timestamp before calling next, await the resolver, then emit the elapsed time together with the field name and parent type. Write it into the per-request metrics collector rather than logging each field individually. Sampling is essential, since per-field logging on a large query is enormous.

537. What are Apollo Server plugins used for?

Plugins hook into lifecycle events such as request start, parsing, validation and execution, which makes them the natural place for tracing, logging, metrics and response shaping. They can observe and modify the request context and the response without touching resolvers. Common examples include usage reporting, cache-control enforcement and error formatting.

538. How would you build a plugin that logs slow operations?

Capture the start time in the request hook, then in the response hook compute the duration and log only if it exceeds a threshold, including the operation name and a hash of the query. Keep variables out of the log or redact them. Emit the same data as a metric so you can alert on slow-operation rate rather than reading logs.

willSendResponse({ operation, response }) {
  if (Date.now() - started > 500) log.warn({ op: operation?.name });
}

539. What does OpenTelemetry add beyond a plugin-based trace?

OpenTelemetry gives you a vendor-neutral standard and automatic instrumentation for the database and HTTP calls a resolver makes. A plugin alone usually produces one span per request, while OpenTelemetry propagates context so downstream spans nest under it. That is what lets you see which resolver caused a slow database call.

540. How do you redact sensitive variables from logs?

Maintain an allowlist of variable names safe to log, or a denylist covering passwords, tokens and personal data, and apply it in one shared logging helper. Redact at the source rather than in the log pipeline, because pipelines are easy to bypass. Also avoid logging raw error messages that may embed values.

541. How do you unit test a resolver in isolation?

Call the resolver function directly with a hand-built parent value, arguments and context, then assert on the returned value or thrown error. Replace the data layer with a stub so the test does not need a database. This keeps the test fast and makes the resolver's contract explicit.

const ctx = { loaders: { userById: { load: jest.fn().resolves(user) } } };
await resolvers.Post.author({ authorId: 1 }, {}, ctx);

542. What should a resolver unit test avoid asserting?

It should not assert on internal SQL, ORM call counts or private helper calls, because those change freely and make the test brittle. It should also avoid asserting on GraphQL serialisation, which is the executor's job. Assert on the resolved value and on observable side effects instead.

543. How do you write an integration test that executes a real query against the schema?

Build the executable schema with the real resolvers, run execute or graphql with a query string and variables, and assert on the full response including errors. Point the data layer at a disposable test database or a container so the query plan is realistic. Seed known data and assert on the exact shape the client will receive.

const res = await graphql({ schema, source: '{ user(id: 1) { name } }', contextValue: ctx });

544. Why should integration tests run against the real schema rather than a hand-built one?

A hand-built schema tests only the parts you remembered to wire up, so missing fields, wrong nullability and broken directives go undetected. The real schema also exercises validation, middleware and error formatting exactly as production does. The trade-off is slower startup, which is usually acceptable for a handful of end-to-end cases.

545. What is a schema snapshot test and what does it catch?

It prints the schema in SDL, stores that text in the repository, and fails when the printed form changes. That catches accidental renames, changed nullability, removed arguments and directive changes that a resolver test would miss. Reviewing the diff in a pull request makes schema changes deliberate.

546. What kind of schema change should a snapshot test fail on?

Any change to the printed SDL, but the important ones are removals and type changes, such as a field becoming non-null or an argument changing type. Additive changes should also fail the snapshot, not because they are unsafe but because a human should confirm the intent. The snapshot is a review gate, not a compatibility oracle.

547. What is a contract test between a GraphQL client and server?

It verifies that the operations the client actually ships are still valid against the server's current schema and return the fields the client code reads. In practice this means running the client's documents through validation, and often through codegen, as part of the server's CI. It catches the case where the server removes a field that no server-side test referenced.

548. How do you keep client and server contracts in sync without deploying both together?

Publish the schema as a versioned artifact and validate client operations against it in the client's CI, so a breaking server change fails the client build before release. Additive changes are safe because they do not affect existing selections. Deprecation windows give clients time to migrate before removal.

549. How do mocks differ from fixtures in GraphQL testing?

A fixture is fixed, hand-written data that represents a specific scenario, so assertions can be exact. A mock is a generated substitute, usually derived from the schema, that fills in plausible values without representing a real case. Fixtures are better for asserting behaviour, mocks are better for unblocking front-end work before the backend exists.

550. When would you use a schema-driven mock generator?

When the schema exists but resolvers or data do not, such as during front-end development or contract-first design. It lets the UI be built against a realistic shape and lets tests exercise every field without writing fixtures by hand. It should not replace integration tests once real resolvers exist.

551. Why can schema-driven mocks hide integration bugs?

Generated values are always present and well-formed, so nullability, empty lists and authorization filtering never occur. The client code that handles those cases goes untested until production. Mocks also cannot reveal that the server returns a different shape than the schema promises.

552. How do you test that a query is rejected by authorization rules?

Execute the operation with a context representing an unauthorized user and assert that the response contains a field error and no leaked data. Test both the null-and-error path and the fully rejected path, because the two are handled differently by clients. Also test that the same query succeeds for an authorized role, so the test proves the rule rather than a syntax error.

553. How do you test subscription delivery in an automated test?

Subscribe on an in-process transport, then publish the matching event and await the next payload. Assert on the received data and on the topic used, and verify that an unrelated event does not deliver. A timeout on the await is important so a broken subscription fails fast instead of hanging the suite.

554. How do you test DataLoader batching behaviour?

Spy on the batch function, resolve several fields that reference different keys in one execution, and assert that the batch function was called once with all keys. Then assert that the mapped values land on the correct fields, which catches ordering bugs. A second test for the same key twice confirms the per-request cache works.

555. How do you test that caching does not leak data between users?

Run the same operation twice with two different authenticated contexts and assert that the second response reflects the second user's data. Use a cache implementation that is shared between the two calls, otherwise the test proves nothing. This test is worth keeping permanently, since cache key regressions are silent and severe.

556. What is the difference between testing a schema and testing an operation?

Schema tests check the contract itself, such as type definitions, directives and whether a document validates. Operation tests check behaviour, such as the data and errors produced for a specific query with specific variables. You need both, because a valid document can still resolve incorrectly and a correct resolver can sit behind a broken schema.

557. How do you test error formatting and partial errors?

Trigger a field error in one branch of a nullable field and assert that other fields still return data, with the error carrying the right path and code. Assert on the public error shape, not on internal exception text. Also assert that stack traces and internal messages are absent in production mode.

{ "data": { "user": { "name": null } }, "errors": [{ "path": ["user","name"], "extensions": { "code": "FORBIDDEN" } }] }

558. Why is coverage per resolver a weak signal in GraphQL?

A resolver can be executed and still be wrong for the interesting cases, such as empty lists, null parents or denied access. Coverage also says nothing about the query shapes that combine resolvers, which is where N+1 and authorization bugs appear. Prefer operation-level tests that represent real client documents.

559. What is GraphQL codegen and what does it generate for TypeScript?

It reads the schema and the client's operation documents and emits TypeScript types for the result and variables of each operation. Many setups also generate typed hooks or document nodes, so a renamed field becomes a compile error. That moves schema mismatches from runtime to build time.

const { data } = useGetUserQuery({ variables: { id: "1" } }); // data.user.name typed

560. What is the difference between generating types from the schema and from operations?

Schema types describe every possible field and are useful on the server or for shared model types. Operation types describe only the selection the client asked for, so they are narrower and correctly reflect nullability and optional fields. Clients should generally use operation types, because a schema type would claim fields exist that the query never requested.

561. What is a TypedDocumentNode and what problem does it solve?

It attaches the generated result and variable types to the parsed document object itself, so a client hook can infer types from the document without an explicit generic. That removes the duplicated type argument that used to drift from the query. It also makes the operation reusable across components with the same inference.

562. How do you keep generated types fresh in CI?

Run codegen as a CI step and fail the build if the output differs from what is committed. That prevents a schema change from merging while stale types stay in the repository. Run it against the schema artifact rather than a live server so the check is deterministic.

563. What is the Apollo Client normalized cache?

It stores each object once, keyed by __typename and id, and stores query results as references into that store. A mutation that returns an updated object therefore updates every query that referenced it. This is what makes most UI updates automatic without a refetch.

564. What do __typename and id do in the Apollo cache?

Together they form the cache key for an object, so the same entity fetched by two different queries resolves to one entry. Without them the object is stored by its position in the query, which cannot be shared. Requesting __typename explicitly is often necessary for fragments and cache matching.

565. What happens when an object has no id and no __typename?

Apollo falls back to a positional key based on the path in the query, so the object cannot be shared or updated by a mutation elsewhere. The cache will hold duplicates of what is logically the same entity and updates appear inconsistent. The usual fix is to add an id to the type or define a custom key field in a type policy.

566. Why can a mutation produce a correct response but a wrong cache?

If the mutation returns a partial object, Apollo merges it into the existing entity, and any field omitted in the response keeps its old value. If the mutation changes a list, the list itself is not updated unless the mutation returns the parent or you write to the cache. The server is right and the client is stale, which is confusing to debug.

567. How do type policies customise cache behaviour?

A type policy can define the key fields for a type, custom merge functions for lists, and field-level read functions. They are essential for types without ids and for paginated fields where a naive merge would overwrite pages. They live on the client, so a policy mismatch with server behaviour causes subtle UI bugs.

typePolicies: { Post: { keyFields: ["slug"] },
  Query: { fields: { feed: { merge: true } } } }

568. What is the difference between refetch and fetchMore?

refetch re-runs the original query with new variables and replaces the cached result. fetchMore runs a follow-up query, usually the next page, and lets you decide how to merge it into the existing data. Use refetch when the data changed, and fetchMore when you are extending the result set.

569. When should you use cache.update or writeQuery instead of refetching?

When the mutation already returns everything needed to update the cache, so a refetch would be a wasted round trip. Writing the change directly gives an instant UI and avoids flicker on slow networks. Refetch instead when the server applies logic you cannot reproduce client-side, such as recalculated totals across many records.

570. What is a common pitfall with refetchQueries after a mutation?

Refetching every active query can trigger a burst of requests and a visible loading flicker across the page. Refetching by query name instead of by the exact variables can also refetch the wrong instances. Often the mutation response already contains the changed entity, so a targeted cache write is cheaper and smoother.

571. How does optimistic UI interact with the normalized cache?

An optimistic response is written to the cache immediately as a temporary layer, so components re-render before the server replies. When the real response arrives the optimistic layer is discarded and replaced, which can cause a visible jump if the values differ. On error the layer is rolled back, so the mutation must be safe to abandon.

572. How does the Relay compiler use fragments and what does fragment ownership mean?

Each component declares the data it needs in its own fragment, and the compiler builds one query per page from the fragments it can reach. Fragment ownership means a component owns its data requirements, so a parent does not need to know which fields a child uses. This keeps changes local, because adding a field only touches the component that reads it.

const PostTitle_post = graphql`fragment PostTitle_post on Post { title }`;

573. Why does Relay require globally unique ids?

Relay's store identifies every record by a global id so the same entity can be referenced from different queries and connections. That is what makes mutations and refetches update all views of the object consistently. Types without a global id are treated as local and cannot participate in that normalisation.

574. What is a urql exchange and how does the exchange pipeline work?

An exchange is a middleware in a pipeline that receives operations and returns results, so behaviour is composed rather than inherited from one client class. Built-in exchanges handle caching, deduplication, retries and subscriptions, and you can insert your own anywhere in the chain. The order matters, because an exchange only sees operations that earlier exchanges pass down.

575. How would you add authentication to every urql request?

Insert a custom exchange that reads the current token and adds an authorization header to the operation context before forwarding it. Reading the token at request time rather than at client creation avoids holding a stale token after refresh. For subscriptions, the same header must be attached during the WebSocket handshake.

const authExchange = ({ forward }) => ops$ => forward(ops$.map(op => {
  op.context.headers = { ...op.context.headers, authorization: getToken() }; return op;
}));

576. How do you implement retry in a GraphQL client without duplicating mutations?

Restrict automatic retries to queries and to network-level failures, because a mutation may already have been applied on the server. For mutations, use idempotency keys or a server-side deduplication window instead of blind retries. Exponential backoff with jitter prevents a failed backend from being hit by a synchronised retry storm.

577. What belongs in a GraphQL error versus the extensions payload?

The message is for developers and should be stable, human-readable and free of internal details, while path and locations identify where it occurred. Machine-readable codes, retry hints and field-level validation details belong in extensions. Business outcomes that a client must branch on are often better modelled as union types in the schema than as errors.

{ "message": "Not authorized", "path": ["post","draft"],
  "extensions": { "code": "FORBIDDEN" } }

578. How do you integrate error reporting with a GraphQL server?

Hook into the error formatting or execution lifecycle, filter to unexpected errors, and forward them with the operation name, a query hash and the error path. Include a request id so a user report can be traced back to the exact execution. Strip variables and headers before sending, since they routinely contain tokens.

579. Why should expected validation errors not go to an error tracker?

Client mistakes such as malformed queries or missing variables are normal traffic and would drown real defects in noise. Track them as metrics instead, since a sudden rise is usually a broken deploy or a bad client version. Reserve the error tracker for errors that indicate a server bug.

580. What is a performance budget for a GraphQL API?

A budget is an explicit limit on cost or latency per operation, such as p95 under 300 ms and a maximum query cost per user per minute. It turns vague performance goals into numbers that CI and monitoring can enforce. Budgets are most useful when they are attached to the operations clients actually send.

581. Which metrics define an SLO for a GraphQL endpoint?

Latency percentiles per operation, error rate split by error code, throughput, and saturation signals such as database time and queue depth. Because one endpoint serves many operations, the SLO should be per operation or per operation class rather than a single endpoint average. Cost-per-request is also worth tracking, since it predicts capacity limits.

582. How do you measure per-field latency in production?

Instrument resolver execution with a timing wrapper and record durations keyed by parent type and field name, sampled to keep overhead low. Aggregate into histograms rather than logs so you can compute percentiles. Because fields nest, attribute the time to the field that actually performed the I/O.

583. What is the risk of using average latency in an SLO?

Averages hide the tail, and in GraphQL the tail is where the user pain is, because slow operations are often the most expensive ones. A healthy mean can coexist with a p99 of several seconds. Percentile-based objectives reflect the experience of real users far better.

584. How do you keep a resolver from blocking the event loop?

Use asynchronous I/O everywhere and never call synchronous CPU-heavy functions such as large JSON parsing, cryptography or image processing on the request thread. Move genuinely CPU-bound work to a worker pool or a separate service. Blocking the event loop stalls every concurrent request, not just the one being served.

585. When do sibling resolvers run in parallel and when are they serialised?

Fields on the same object are resolved concurrently, which is why DataLoader can batch them. Mutations at the root are executed serially in document order, because each may depend on the previous one. A parent field must finish before its children start, since the children need the parent value.

586. How do you cache a full query result keyed by operation and variables?

Hash the normalized document together with the sorted variables, then store the serialized response under that key with a short TTL. Sorting matters because two logically identical variable objects can serialise differently. The key must also include anything that changes the output, such as the tenant or the authorization level.

587. What invalidates a query-result cache?

Time-based expiry is the simplest approach, but it trades freshness for hit rate. Event-based invalidation tags entries by the entities they touched and clears those tags when a mutation affects them. Without a clear invalidation story, a response cache becomes a source of stale, hard-to-reproduce bugs.

588. How do you handle cache invalidation after a mutation?

Have the mutation publish the entity types or ids it changed, and delete the cache entries tagged with them. Doing this after the transaction commits avoids a race where a reader repopulates the cache with pre-commit data. If invalidation cannot be made reliable, keep TTLs short instead of pretending the cache is consistent.

589. What is the difference between a warm cache and a cold cache in performance testing?

A cold run measures the true first-request cost including connection setup, planning and data loading, which is what users see after a deploy or an idle period. A warm run measures steady-state cost and tends to look much better. Load tests should report both, because autoscaling and timeouts usually fail on the cold path.

590. What is the purpose of the @key directive in Apollo Federation?

It declares which fields identify an entity so that other subgraphs can reference it by those fields alone. The gateway uses that key to fetch the entity from the subgraph that owns it. A type can have multiple keys, which lets different subgraphs reference it with different identifiers.

type Product @key(fields: "id") { id: ID! name: String }

591. How does a gateway resolve an entity reference across subgraphs?

It sends an entity representation containing the key fields to the owning subgraph, which returns the remaining fields. The response is merged with the other subgraph data under the same entity. This is why every referenced entity needs a resolver that can look itself up by key.

{ __typename: "Product", id: "1" }  ->  reviews subgraph

592. Why does federation make N+1 worse and how do you mitigate it?

A single query can trigger one entity fetch per item per subgraph, multiplying the round trips across the network instead of just the database. The gateway should batch entity representations, and subgraphs must expose a batch entity resolver rather than a single-item one. Instrumenting gateway-to-subgraph calls per operation is the way to see the problem.

593. What is the difference between schema stitching and federation?

Stitching merges remote schemas in a gateway and routes fields using custom logic or type merging rules. Federation makes the composition rules part of the schema itself through directives and entity keys, so subgraphs declare how they combine. Federation scales better across teams and languages, while stitching is simpler for wrapping a few existing APIs.

594. How do you evolve a GraphQL schema without versioned endpoints?

Add new fields and arguments incrementally, deprecate what is going away, and only remove it once usage data shows no clients depend on it. Because a single endpoint serves all clients, additive change is the only reliably safe move. If you must make a breaking change, introduce a new field or type and migrate consumers.

595. What counts as a breaking change in a GraphQL schema?

Removing a field or type, making a nullable field non-null, removing an enum value, or changing an argument's type are all breaking for some client. Adding a required argument is also breaking, since existing documents do not supply it. Adding an optional field or an enum value is generally safe, though clients with exhaustive switches must handle it.

596. How do you deprecate a field safely?

Mark it with @deprecated and a reason that names the replacement, then monitor field usage to confirm clients have migrated. Keep returning the old value until usage reaches zero or an agreed sunset date. Removing it earlier turns a warning into an outage for whichever client you did not know about.

oldName: String @deprecated(reason: "Use displayName instead.")

597. How can an attacker abuse an endpoint without introspection?

They can send deliberately invalid queries and read validation errors, which often name the unknown field and suggest similar ones. Field suggestions effectively reconstruct the schema one request at a time. That is why disabling suggestions and returning generic messages matters as much as disabling introspection.

598. Why is disabling field suggestions important in production?

The classic "Did you mean X?" message confirms the existence of fields and reveals naming conventions. Combined with a few requests, it lets an attacker enumerate the schema without introspection. Turn suggestions off for untrusted traffic while keeping them in development.

599. What is the role of a gateway or query planner in GraphQL performance?

The planner decides which subgraphs to call, in what order, and which parts can run in parallel. A poor plan serialises calls that could be parallel or fetches fields that are never used. Plan complexity is also why a gateway can be the bottleneck even when every subgraph is fast.

600. What is the difference between error masking and error propagation?

Masking replaces an internal error with a safe message before it reaches the client, which prevents leaking stack traces and SQL. Propagation decides how far the null travels: a non-null field error bubbles up to the nearest nullable parent. Both behaviours are configurable, and the default is not always what you want.

601. Why should a null bubble not be used to hide authorization failures?

When a non-null field throws, the null propagates upward and can wipe out an entire object or the whole response, which looks like an outage rather than a permission problem. It also makes the failure hard to distinguish from a genuine missing record. Return an explicit error code and, where possible, model the outcome in the schema.

602. How do you return partial data while reporting a field error?

Throw inside a nullable field so the error is recorded at that path and the rest of the response still resolves. The response then contains both data with a null at that field and an errors entry. Clients must be written to handle that combination rather than treating any error as a total failure.

603. How would you enforce pagination on list fields schema-wide?

Adopt a connection type for every list that can grow, validate the arguments in shared middleware, and reject list fields that lack pagination arguments during schema linting. A schema rule in CI is more reliable than code review, because new fields are added constantly. Defaults should also be enforced, not just maximums.

604. What is the difference between a mutation and a query with respect to caching?

A query is expected to be free of side effects and can therefore be cached, retried and sent over GET. A mutation changes state, so it must never be cached, must not be retried blindly, and should be sent over POST. The distinction is a contract, and resolvers that write data inside query fields break every cache in the chain.

605. Why must mutations be executed serially?

Two mutations in one document may depend on each other, so the specification requires them to run in the order they appear. Running them concurrently would make the outcome depend on timing. For the same reason, clients should not rely on batching to parallelise writes.

606. How do you make a mutation idempotent?

Accept a client-generated idempotency key, store it with the result, and return the stored result if the same key arrives again. The record and the key must be written in one transaction, otherwise a crash between them allows a duplicate. This is what makes retries after a network timeout safe.

607. What is the role of client-generated ids in mutation idempotency?

A client-generated id lets the server recognise a retry of the same create request rather than treating it as a new one. It also enables optimistic UI, because the client can reference the entity before the server responds. The id must be validated for format and uniqueness scope, since it is client-controlled.

608. How do you test that a mutation invalidates the client cache correctly?

Render a component that reads the affected query, perform the mutation through the real cache, and assert that the component shows the new data without a refetch. This catches missing __typename, wrong key fields and merge policies that silently drop the update. Testing through the cache rather than the network is what makes the test meaningful.

609. What is the difference between a subscription event payload and the subscription response?

The event payload is what the publisher sends to the topic and becomes the root value for execution. The subscription response is the result of running the client's selection set against that value, so it can differ in shape and can include nested lookups. Sending a minimal payload and resolving the rest is usually better than publishing a fully expanded object.

610. How do you filter subscription events per subscriber?

Include the filter in the subscription arguments, derive a topic or channel from them, and only register the subscriber for matching events. Filtering after delivery wastes work and risks leaking an event the subscriber should never have received. The authorization check must also run when the event is delivered, not only when the subscription starts.

subscription { orderUpdated(orderId: "42") { status } }

611. Why is authorization for subscriptions harder than for queries?

A query is authorized once at execution time, but a subscription must be authorized at subscribe time and again for every event, because permissions can change while the connection stays open. A long-lived connection with an expired token is a real risk. Re-check on each delivery and close the subscription when access is revoked.

612. What happens to in-flight subscriptions during a rolling deploy?

Connections to terminating instances drop, and clients must reconnect with backoff to another instance. Any events published while the client is disconnected are missed unless the broker retained them. Clients should refetch current state on reconnect rather than assuming the stream was continuous.

613. How do you limit concurrent subscriptions per connection?

Count active operations per socket and reject new ones beyond a configured maximum, returning a clear error the client can handle. The limit protects memory and per-connection fan-out, both of which scale with operation count. Also cap the total number of connections per user or IP to stop a client from opening many sockets instead.

614. What is the risk of trusting the operation name in metrics?

The operation name is supplied by the client, so it can be missing, duplicated across different documents, or arbitrarily varied to create cardinality explosions in a metrics system. Dashboards then show misleading per-operation latency and alerting misfires. Key metrics on a hash of the normalized document and treat the name as a label with a bounded allowlist.

615. How do you handle anonymous operations in monitoring?

Fall back to the document hash as the identity so anonymous operations still group correctly. Alert on the share of anonymous traffic, since most production clients should be using persisted queries. Never build metric labels directly from the raw query text.

616. What is a reasonable schema linting strategy in CI?

Enforce naming conventions, require descriptions on public types, ban list fields without pagination, and run a breaking-change check against the last released schema. Linting catches the cheap mistakes so reviewers can focus on design. Keep the rules in the repository so they apply identically on every machine.

617. How would you review the trade-off between flexibility and protection in a GraphQL API?

Every field you expose increases both client capability and attack surface, so the question is which queries you can afford to serve. Protect the expensive and sensitive parts with cost limits, pagination and field authorization rather than removing them. Measure real usage, because a flexible field nobody uses is pure risk.

618. What is the difference between a resolver and a data source?

A resolver maps a field to a value and contains the field-level logic, while a data source encapsulates access to a backing system such as a database, REST service or cache. Separating them keeps resolvers thin and makes the data access layer testable and reusable across fields. It also gives you one place to add batching, retries and caching.

619. How do you decide between exposing a REST endpoint and a GraphQL field?

Prefer a GraphQL field when clients need varying selections of the same underlying data, and a REST endpoint when the consumer is not a GraphQL client or the payload is a binary stream. Public third-party consumers often find REST easier to integrate. The two can coexist, with GraphQL fields wrapping internal REST calls.

620. What does a good GraphQL error message look like to a client developer?

It states what went wrong and what to do next, without exposing internals, and carries a stable machine-readable code. It should identify the offending field or argument so the developer can locate the problem. Internal details belong in logs and traces, correlated by a request id.

Advanced 377 questions

Performance engineering, cost and depth limits, hardening, federation and schema governance, implementations (HotChocolate, Apollo, Yoga, gqlgen, GraphQL.NET), delivery and migration.

621. How does DataLoader actually batch work within a single event loop tick?

Each DataLoader instance keeps an in-memory cache and a pending batch queue. On the first load(key) call it schedules a dispatch with process.nextTick (or a microtask), then any further load calls for distinct keys within that same window are appended to the same queue. When the scheduled callback runs, the batched keys are passed once to the batch function, and every pending promise is resolved from the returned array. This is why batching depends on resolvers yielding to the event loop rather than awaiting one key at a time.

622. Why does awaiting a DataLoader load inside a loop on the same key still produce multiple cache hits but one batch entry?

The cache is keyed by the serialized key, so a repeated key returns the memoized promise and never re-enters the batch function. Because the promise is created eagerly at load time, a sequential await loop still enqueues only unique keys. The typical failure case is calling load with a new object each iteration, which produces a different cache key and therefore a new batch entry.

623. What determines when DataLoader flushes, and how can scheduling drift break batching?

Flushing is driven by the batch scheduling function, which defaults to a next-tick callback. If any code path performs synchronous or microtask-heavy work between load calls, the dispatch may already have run, splitting what looked like one batch into several. Long synchronous resolver chains, timers, or setImmediate boundaries are the usual causes of drift.

const loader = new DataLoader(batchFn);              // default: process.nextTick

624. How do you design a DataLoader cache key for a composite lookup such as tenant plus id?

Serialize every input that affects the result, deterministically and unambiguously, into a single string key. A separator that cannot appear in the values, or JSON with sorted keys, avoids collisions such as "a:b"+"c" versus "a"+"b:c". Never key on object identity, because two structurally equal inputs would miss the cache and split the batch.

const key = (tenantId, id) => `${tenantId}\u0000${id}`;

625. Why must DataLoader instances be created per request rather than shared globally?

The cache holds resolved values, so a shared instance leaks data across users and returns stale results after mutations. Request-scoped instances also make isolation trivial to reason about: when the request ends, the cache is garbage collected. Only caches of truly public, immutable data are safe to share, and even then they need their own TTL.

626. What is batch poisoning and how does a single failing key affect other requests?

Batch poisoning occurs when one bad key causes the batch function to throw, rejecting the whole batch and therefore every caller in it. Attackers exploit this by supplying input that makes a shared backend query fail, degrading unrelated users. The defense is to catch per-key errors inside the batch function and return an Error in that array position instead of throwing.

return rows.map((r, i) => r ?? new Error(`not found: ${keys[i]}`));

627. How do you prevent one slow key from delaying an entire DataLoader batch?

Bound the underlying call with a timeout and map a timeout to a per-key error rather than letting the promise hang. Alternatively, split the batch into chunks, or fall back to an individual fetch for keys that exceed a latency threshold. Without a bound, one pathological key sets the latency floor for every request sharing the batch.

628. What is the risk of enabling DataLoader caching for authorization decisions?

Authorization results are sensitive to context such as the current user, tenant, and token claims, so a cache key that omits them yields cross-tenant authorization bypass. Even with a correct key, caching a positive decision across a token refresh can outlive the permission. Keep auth checks uncached, or key them on an immutable subject identifier plus a policy version.

629. How do you batch across resolvers that run in different microtasks when the schema cannot be changed?

Introduce a request-scoped flush barrier: collect keys during resolution and resolve them in a single deferred phase before serialization completes. In practice this means the loader is created per request and dispatched explicitly at the end of each execution phase, or the resolvers are restructured to return thunks. Purely syntactic tricks cannot batch across an await that has already forced the previous dispatch.

630. What is the difference between per-request and per-operation DataLoader isolation in a batching gateway?

Per-request isolation scopes the loader to a single HTTP request, which may contain multiple operations in a batch. Per-operation isolation scopes it to one operation within that batch, preventing one operation's cached reads from influencing another's results. When operations can come from different users, per-operation scoping is the safer default.

631. How do you detect alias amplification in an incoming GraphQL operation?

Count the number of field selections per response path after fragment expansion, not the number of textually distinct fields. Aliases let a client request the same expensive field hundreds of times under different names, inflating resolver work while keeping the query text short. Static analysis that normalizes aliases to their target field exposes the amplification factor.

query { a: expensive(id: 1) a2: expensive(id: 2) /* ... x500 */ }

632. Why does depth limiting alone fail to stop alias amplification?

Depth limiting measures nesting, and an amplified query is flat but wide. A query of depth two with a thousand aliases passes any depth rule while still costing a thousand resolver invocations. Effective protection must combine breadth or cost accounting with depth limits.

633. Compare static cost estimation with runtime cost measurement for GraphQL query cost analysis.

Static estimation walks the parsed document and assigns weights per field, multiplied by declared list sizes and arguments such as first. It is cheap, deterministic, and can reject before execution, but it must guess list cardinality and cannot see actual data. Runtime measurement counts real resolver calls and rows returned, which is accurate but arrives after the work is already done, so it is best used for budgets, logging, and adaptive throttling rather than pre-execution rejection.

634. How do you estimate the cost of a paginated field without trusting the client's first argument?

Clamp first to the server maximum before using it in the estimate, and apply a default when it is absent. Estimate cost as maxRows x childCost, where maxRows is the clamped value or a schema-declared upper bound. Otherwise a client sets first: 100000, gets rejected only by the resolver, and the cost model under-reports the true risk.

635. How do cost directives work and what makes a good custom cost directive?

A cost directive annotates fields or arguments with a weight or a cardinality multiplier that the analysis layer reads. A good directive is schema-declared, so it travels with the type, and supports composition such as @cost(weight: 5) plus argument-based multipliers. It must be enforced by tooling on every schema change, or new expensive fields silently default to weight one.

type Query { search(q: String!, first: Int! @cost(multiplier: true)): [Hit!]! @cost(weight: 20) }

636. How would you implement per-client cost budgets?

Track a rolling counter keyed by client identity, and deduct the estimated or measured cost of each operation. Reject when the projected cost would exceed the budget for the window, and return a machine-readable error the client can handle. Budgets should be tiered by plan and adjustable without a deploy, with server-side defaults that fail closed.

637. What is a fragment cycle attack and how do you defend against it?

A malicious document defines fragments that reference each other, so naive expansion recurses until the parser or analyzer exhausts stack or memory. Spec-compliant validation rejects cyclic fragment spreads, but validation itself can be the vector if it expands eagerly. Defend by detecting cycles during a linear traversal that marks visited fragments, and by enforcing a hard cap on fragment spread depth.

fragment A on User { ...B }  fragment B on User { ...A }

638. How do you bound the total work of fragment expansion?

Expand fragments once into a normalized selection tree and reuse that structure, rather than re-expanding per occurrence. Track the expanded node count and abort with a validation error when it exceeds a configured ceiling. This turns an exponential blowup into a linear pass with an early exit.

639. Why is introspection hardening important in production?

The full introspection result discloses every type, field, argument, deprecation note, and description, which shortens an attacker's reconnaissance considerably. Descriptions often leak internal identifiers, feature-flag names, or planned fields. Disabling introspection in production is a reasonable default, though it is obscurity rather than a security boundary.

640. How can you disable introspection without breaking tooling?

Reject __schema and __type at the validation layer for untrusted clients while allowing an authenticated internal role. Static schema artifacts generated at build time give editors and codegen the types without runtime introspection. This preserves developer experience and removes the public disclosure surface.

641. What is the security value of disabling field suggestions in error messages?

Did-you-mean suggestions let an attacker enumerate field names one character at a time by observing which typo produces a suggestion. Disabling suggestions in production removes an oracle that reconstructs the schema without introspection. Keep the suggestions in development where they help developers.

642. What else leaks in GraphQL error responses besides field names?

Stack traces, database error text, internal service names, and extensions payloads are all commonly forwarded verbatim. Validation errors can also reveal argument requirements and enum values. Production responses should carry a stable error code and a safe message, with the detail written only to internal logs correlated by a request id.

643. What is Automatic Persisted Queries and what problem does it solve?

APQ lets a client send only a hash, and the server responds with PersistedQueryNotFound the first time so the client resends the full document, which the server then stores by hash. Subsequent requests are tiny and cannot be arbitrary documents, shrinking payloads and closing most of the ad-hoc query surface. It is an optimization plus a partial allowlisting mechanism, not a complete one, unless unknown hashes are never registered.

{ "extensions": { "persistedQuery": { "version": 1, "sha256Hash": "..." } } }

644. How does APQ differ from a strict persisted query allowlist?

APQ allows a client to register any document on first use, so a novel query still executes once it is hashed. A strict allowlist accepts only hashes that were published at build time, rejecting everything else outright. The allowlist is the security control; APQ is mainly a transport optimization.

645. How do you roll out persisted queries to a large client fleet safely?

Run in a permissive mode that accepts full documents and records their hashes, then switch to allowlist enforcement once coverage is high. Give clients a clear error code so they can fall back and report the missing hash. Monitor the rejection rate by client version before and after the flip, because old app versions cannot be updated quickly.

646. How are trusted documents generated and verified in CI?

Extract every operation used by clients from the source tree, normalize the document text, hash it, and emit a manifest of hash to document. CI compares the manifest to the pinned one and fails on drift, and the gateway loads only hashes present in the signed manifest. Signing the manifest prevents an attacker with repository write access from quietly adding a query.

647. Why must persisted query hashes be computed over a normalized document?

Whitespace, comments, and field ordering variations change the raw bytes but not the semantics, so hashing raw text causes spurious misses. Normalization must be deterministic and applied identically by client and server. If normalization differs even slightly, every request degrades to a cache miss and the allowlist becomes unusable.

648. At what granularities can you rate limit a GraphQL endpoint, and what does each miss?

IP-based limits stop crude floods but punish shared NATs and are useless against distributed clients. User-based limits align with accountability but miss unauthenticated traffic. Tenant-based limits protect noisy-neighbor isolation. Operation-based limits target a specific expensive query. A layered scheme combining all four, with cost units rather than request counts, is the realistic answer.

649. Why is request-count rate limiting a poor fit for GraphQL?

One request can carry arbitrarily large cost depending on aliases, pagination, and nesting, so counting requests treats a trivial query and a catastrophic one as equal. Rate limits should be denominated in estimated or measured cost. Otherwise the cheapest way to attack is a single very expensive request.

650. How do concurrency limits and queueing interact with tail latency?

A concurrency cap protects downstream services but converts overload into queue time, which shows up directly as p99 latency. An unbounded queue hides the overload until the queue itself becomes the outage. Bound the queue, reject on overflow with a retry hint, and shed load by priority rather than first-come order.

651. How do you propagate cancellation from a client disconnect through resolvers?

Create an AbortController per operation, abort it when the transport closes, and pass its signal into the context so data sources can observe it. Downstream HTTP clients and database drivers must accept the signal; otherwise the resolver returns early while the work continues. Check signal.aborted before starting expensive work, not only after.

const ac = new AbortController();
req.on('close', () => ac.abort());
const ctx = { signal: ac.signal };

652. Why is a resolver-level timeout insufficient to bound an operation?

A timeout on one resolver does not bound the sum of many resolvers, and abandoned promises keep consuming resources after the timeout fires. You need an operation-level deadline as well, derived from the request start, plus cancellation so timed-out work actually stops. Timeouts without cancellation are a latency patch, not a resource bound.

653. How do you guard response size when a query returns many small fields across many rows?

Cap rows at the source with pagination limits, then measure serialized byte size and terminate the response once it crosses a threshold. Estimating size from field count alone underestimates long strings and nested objects. A hard byte ceiling with a clear error protects the process from building a multi-hundred-megabyte string.

654. What is the real cost profile of a large first: N pagination argument?

Cost scales with rows times the cost of the child selection, plus sorting and any total-count computation. A large first on a field with expensive children is multiplicative, not additive. It also inflates memory because all rows are materialized before serialization.

users(first: 5000) { posts(first: 50) { comments(first: 50) { body } } }

655. How do you validate opaque cursors against tampering?

Treat cursors as untrusted input: base64-decode, parse, and validate the shape before use. Sign or encrypt them so a client cannot forge an arbitrary offset or inject values into the underlying query. Encode the sort key and direction in the cursor and reject cursors that do not match the requested ordering.

656. What goes wrong if cursors are plain base64 of an internal id?

Any client can decode, modify, and re-encode them, which enables enumeration of other tenants' rows and breaks pagination invariants. Base64 is encoding, not protection. Use authenticated encryption or an HMAC over the payload with a server-held key.

657. How do cyclic schemas enable deep nesting attacks?

A type that references itself lets a client write an arbitrarily deep selection set, each level triggering another resolver and often another query. Depth limits cap this, but the cap must account for fragments and inline fragments that hide the apparent depth. Cost per level also matters, since a shallow but wide cycle can be equally expensive.

query { me { friends { friends { friends { friends { id } } } } } }

658. How do you set a defensible maximum depth?

Measure the depth distribution of real client operations and set the limit above the observed p99 with headroom. A fixed number copied from a blog post either breaks legitimate queries or permits abuse. Re-measure after schema changes, since new relationships change the legitimate depth profile.

659. How can a custom scalar become a denial-of-service vector?

A scalar that parses arbitrary input with an unbounded or backtracking parser can consume CPU on a tiny payload. Classic cases are a regex with nested quantifiers on a long string, or a JSON scalar that accepts deeply nested structures. Bound input length, reject pathological nesting, and avoid user-supplied patterns entirely.

660. Why is a catastrophic-backtracking regex dangerous in a resolver or scalar?

Runtime grows exponentially with input length, so a few kilobytes can occupy a worker for minutes. If the pattern includes user-controlled alternation the attacker tunes the input to maximize backtracking. Prefer linear-time validation, anchor patterns, and hard length caps before matching.

/^(a+)+$/   // on "aaaa...!" this backtracks catastrophically

661. How do you block field suggestions at the schema level rather than by filtering output?

Supply a custom validation rule set that does not include the suggestion-producing rule, so the error never contains candidates. Alternatively, replace the rule with a version that emits only the field path. Filtering the message string afterwards is brittle because formats change between library versions.

662. How do you protect mutations from replay attacks?

Require an idempotency key per logical mutation and store it with the result for a bounded window, returning the stored result on replay. For state-changing operations where duplicates are harmful, also require a server-issued nonce that is consumed once. The key must be scoped to the authenticated subject, or one user can replay another's mutation.

mutation Pay($key: ID!) { pay(input: {...}, idempotencyKey: $key) { id } }

663. What makes an idempotency key implementation incorrect?

Storing the key after the side effect instead of before it leaves a window where a retry duplicates the operation. Storing it without the request fingerprint lets a different payload reuse the same key and receive a misleading cached response. And an unbounded retention window turns the key store into an unbounded growth problem.

664. At which levels can authorization be enforced in GraphQL, and what are the tradeoffs?

Operation-level checks are cheap and central but coarse, since one operation may touch many resources. Type-level checks handle broad classes of data but cannot express per-row rules. Field-level checks are precise but easy to forget on a new field, so they need linting or schema-level enforcement. Most production systems combine operation gating with field-level checks for sensitive data.

665. Why is authorization in the resolver body risky compared with a schema-level hook?

A resolver-body check is opt-in, so any new resolver that omits it is silently unprotected. A directive or middleware applied by schema construction fails closed and is visible in the schema. Defense in depth uses both, but the structural control is what prevents the forgotten case.

666. How do you cache authorization decisions without creating stale grants?

Key the cache on subject, resource, action, and a policy version, and invalidate by bumping the version when roles change. Short TTLs bound the damage from missed invalidation. Never cache a decision across a token refresh without including the token's claims version.

667. What does context leakage across tenants look like in a GraphQL server?

A module-level variable, a shared singleton, or an improperly scoped DataLoader carries one tenant's identity into another request's resolvers. Because context is often built once and captured in closures, a single missed parameter propagates the wrong tenant everywhere. Enforce tenant id in every data-access call signature so omission is a type error, not a silent default.

668. How do you prevent a query from reading another tenant's rows when ids are globally unique?

Scope every query by tenant at the data layer, not by filtering after fetch. Globally unique ids make cross-tenant reads succeed silently if the tenant predicate is missing. Many teams encode the tenant in the id and verify it matches the context before querying.

669. How do you keep secrets out of logs and traces when variables are logged for debugging?

Maintain a denylist of argument names and a pattern matcher for token-shaped values, and redact before the record leaves the process. Prefer logging operation name, fingerprint, and a hashed variable digest rather than raw variables. Redaction must happen at the logging boundary so every sink inherits it.

670. Why are error paths a PII risk in GraphQL responses?

Error paths identify the exact field and, in some servers, include argument values or upstream messages containing user data. A validation error can echo a submitted email or phone number back in the message. Sanitize error extensions and never include raw upstream payloads in client-facing errors.

671. How does a GET-based GraphQL endpoint enable CSRF?

A GET request carrying a query can be triggered by an image tag or link, and if authentication is cookie-based the browser attaches credentials automatically. The attacker does not need to read the response for a mutation to have effect. Requiring POST for mutations, plus a CSRF token or SameSite cookies, closes this.

<img src="https://api.example.com/graphql?query=mutation{deleteAccount}">

672. How should CORS be configured for a cookie-authenticated GraphQL API?

Return an explicit origin allowlist rather than a wildcard, and set Access-Control-Allow-Credentials only for those origins. Wildcard origin with credentials is rejected by browsers, and reflecting arbitrary origins defeats the protection. Preflight caching should be long enough to avoid per-request OPTIONS overhead.

673. What must JWT validation in the GraphQL context actually check?

Signature against the correct key, algorithm pinned to the expected one, issuer, audience, expiry with small clock skew, and any required scopes. Skipping audience allows a token minted for another service to be replayed here. Validation belongs in context construction, before any resolver runs.

jwt.verify(token, key, { algorithms: ['RS256'], audience: 'graphql-api', issuer: 'https://idp' });

674. Why is accepting the algorithm declared in the token header dangerous?

An attacker can set alg: none or swap RS256 for HS256 and sign with the public key as an HMAC secret. The verifier must pin the allowed algorithms rather than reading them from the token. This is a well-known class of JWT confusion bug.

675. What happens to an active subscription when the access token expires?

Without handling, the WebSocket stays open and the subscription keeps receiving data long after the credential expired. The server should track the token expiry, re-authorize at that moment, and close the connection or send an auth error. Silent continuation is a privilege that outlives its grant.

676. How do you authorize each message on a subscription rather than only at connect time?

Authorize the subscribe operation when it is registered, and re-check authorization for each emitted payload against current permissions. If the user loses access to a resource mid-stream, the next event must not be delivered. A per-event filter that consults the policy engine is the reliable pattern.

677. How should the WebSocket authentication handshake be designed?

Authenticate during the connection init message or via a short-lived ticket in the URL, not via a long-lived token in a query string that lands in logs. Validate the credential before accepting any subscribe message. Bind the connection to the authenticated subject and reject operations for other subjects.

connection_init { "authorization": "Bearer <short-lived-ticket>" }

678. Why is putting a bearer token in a WebSocket URL risky?

URLs are recorded by proxies, load balancers, and browser history, so the token leaks to systems that never needed it. Query-string tokens also bypass header-based redaction tooling. Use a one-time ticket or the init payload instead.

679. How do you limit connection counts and keep subscriptions alive?

Cap concurrent connections per identity and per IP at the gateway, and reject new ones with a clear close code when over. Use ping/pong heartbeats with a dead-connection timeout so half-open sockets are reclaimed. Also cap subscriptions per connection, since one socket can register thousands.

680. How can subscriptions exhaust server resources even with few connections?

Each subscription holds a live query and often a database listener or an in-memory filter, so cost is per subscription rather than per request. A client can register many high-frequency subscriptions on one connection and multiply fan-out. Cap subscriptions per connection, cap event rate per subscription, and drop slow consumers.

681. How do you stop a slow subscriber from consuming unbounded memory?

Give each connection a bounded outbound queue and disconnect when it overflows instead of buffering indefinitely. Backpressure is the only sustainable answer, because a slow reader will otherwise accumulate events forever. Report the disconnect reason so clients can resubscribe deliberately.

682. How do you trace a GraphQL operation across resolvers and downstream services?

Start a span for the operation and create child spans per resolver, propagating trace context into every outbound call. Attach the operation name, fingerprint, and persisted query hash as span attributes for correlation. Without resolver-level spans, a slow operation shows only as one opaque block.

683. What trace attributes make GraphQL traces actually useful?

Operation type and name, normalized fingerprint, client name and version, persisted query hash, error count, and row counts per data source call. Field path on resolver spans is essential for locating the hot field. Avoid raw variables, which are large and often sensitive.

684. How do you choose a sampling strategy for high-volume GraphQL traffic?

Use head-based sampling at a low rate for baseline coverage, plus tail-based sampling that always keeps errors and slow traces. Hash on the operation fingerprint so all traces of one operation are consistently sampled or not. Always-sample rules for low-volume but critical operations prevent blind spots.

685. How do you measure resolver latency and compute a trustworthy p95?

Instrument each resolver call with a monotonic timer and record a histogram rather than an average, because averages hide the tail. Store buckets per resolver field and per operation fingerprint, then compute percentiles from the merged histogram over a fixed window. Exclude trivial resolvers from the alerting set so a fast p95 on cheap fields does not mask a slow one elsewhere.

histogram.observe({ field: info.path.key, op: opName }, ms);

686. What should a slow query log contain for GraphQL operations?

The normalized fingerprint, operation name and type, total duration, per-field timing breakdown, authenticated subject, client version, and result size. The fingerprint groups structurally identical queries so one noisy client is distinguishable from a systemic regression. Log the raw document only behind a strict redaction and access policy.

687. How do operation fingerprints work and why normalize before hashing?

A fingerprint hashes the document after stripping whitespace, comments, and literal values so that user(id: 1) and user(id: 2) collide to the same shape. This groups traffic by query structure, which is what you want for latency percentiles and cache keys. Normalizing literals away means the log cannot reconstruct arguments, which is a privacy benefit.

688. How do you instrument cache hit ratio for resolver-level caches?

Count hits, misses, and negative hits separately, labeled by loader or cache name. A high hit ratio with high latency still points at cold-key stampedes, so also track miss cost. Report the ratio per operation, because a global ratio hides operations that always miss.

689. How do you detect a cache stampede on a hot DataLoader key?

Watch for correlated latency spikes right after a cache expiry, and for many concurrent identical backend calls in a short window. Request coalescing via the loader's memoized promise usually absorbs it within one request, but not across requests. Cross-request stampede protection needs single-flight at the data-source layer.

690. How would you capacity plan a GraphQL gateway?

Model cost per operation from measured resolver counts and downstream calls, then size for peak cost units per second rather than requests per second. Add headroom for the fan-out multiplier of the heaviest allowed operation. Validate the model with load tests that replay real operation mixes, since synthetic uniform traffic misleads.

691. Why is replaying a realistic operation mix essential in GraphQL load testing?

GraphQL cost varies by orders of magnitude across operations, so a test that hammers one simple query proves almost nothing. Replay a distribution derived from production fingerprints with realistic proportions and concurrency. Include the heavy tail explicitly, because that is what breaks the system.

692. How do you load test pagination and nested queries without corrupting data?

Run against an isolated environment with a production-shaped dataset, and make write operations target synthetic tenants. Read-heavy tests still need realistic cardinality, or the database plan differs from production. Reset or snapshot the dataset between runs so results are comparable.

693. What does chaos testing resolver failures look like in practice?

Inject latency and errors at the data-source boundary for selected resolvers, then verify that partial results, nullability handling, and error extensions behave as designed. Test both a flaky dependency and a fully down dependency. The goal is to confirm that one failing field degrades gracefully rather than failing the whole response.

if (chaos.shouldFail('users')) throw new Error('injected');

694. Why is nullability a critical part of failure-mode design in GraphQL?

A null propagating up a non-null chain nulls the entire parent, so one failing resolver can erase an otherwise healthy response. Marking fields non-null should reflect a real invariant, not optimism. Chaos tests are how you find out which chains are brittle before an incident does.

695. What makes schema validation in CI idempotent and trustworthy?

Generate the schema from source deterministically, sort all elements, and compare against a committed artifact so reruns produce identical output. Pin tool and dependency versions, since formatter changes create false diffs. A clean run must fail only on real schema changes, not on environment drift.

696. How do you catch breaking schema changes before deploy?

Diff the proposed schema against the deployed one and classify changes as breaking or safe using a rule set: removing a field, tightening nullability, or removing an enum value is breaking. Run the check on every pull request and require an explicit override with a deprecation plan. Track field usage from real operations to judge impact.

697. What belongs in a security review checklist for a GraphQL API?

Introspection and suggestion exposure, query depth and cost limits, alias and fragment expansion caps, authorization coverage per field, persisted query enforcement, rate limiting by identity and cost, request and response size caps, timeout and cancellation, log redaction, and CSRF/CORS posture for cookie auth. Each item needs an owner and a test, not just a policy statement. Review it whenever the schema or the auth model changes.

698. How do you test that every field is authorized?

Enumerate fields from the schema and assert that each one either has an authorization directive or appears on an explicit public allowlist. A CI test fails when a new field lacks both, which catches the forgotten-resolver case. Complement it with negative integration tests using a low-privilege principal.

699. How do you bound work when resolvers fan out to many downstream services?

Set a per-operation call budget in addition to a time budget, so a query cannot issue hundreds of downstream requests even if each is fast. Combine with batching to collapse identical calls. Exceeding the budget should produce a deterministic error rather than a partial, unpredictable result.

700. What is the danger of using list length in the cost model as an unbounded value?

If the model multiplies by an unbounded list size, the estimate becomes attacker-controlled and either trivially passes or trivially fails. Every multiplier must be clamped to a server-known maximum. Treat client-supplied pagination arguments as hints, never as ground truth for cost.

701. How do you protect an internal GraphQL gateway from a compromised service account?

Scope service tokens to specific operations and tenants, and enforce the same cost and depth controls for machine clients. Internal traffic should not bypass the gateway's admission checks. Audit-log the subject on every operation so a compromised credential is traceable.

702. What is the risk of trusting a client-supplied operation name in logs?

Operation names are arbitrary strings, so a client can inject misleading names, huge payloads, or log-forging characters. Always sanitize and length-cap before logging, and treat the fingerprint as the real identity. Alerting keyed on a spoofable name is trivially evaded.

703. Why is mixing identities in one HTTP batch a security problem?

If operations in a batch are authorized from a single context, one principal's credentials may apply to another's operation. Even with per-operation context, shared caches and loaders can cross-contaminate results. The simplest correct rule is one identity per batch.

704. How do you detect that a client is probing for schema fields?

Count validation errors per identity in a short window, especially errors containing a path that does not exist. A burst of near-miss field names is the signature of enumeration. Alert on the pattern and throttle the identity, rather than only returning errors.

705. Why does disabling introspection not stop schema enumeration?

Error messages, suggestions, and behavioral differences still reveal field existence, and the schema is often public in client bundles. Introspection hardening raises the cost of discovery but is not a boundary. Real protection comes from authorization on every field and from not shipping secrets in schema descriptions.

706. How do you prevent schema descriptions from leaking internal information?

Review descriptions as part of the schema diff, and strip internal-only annotations before generating the public artifact. Descriptions frequently accumulate ticket references, internal hostnames, and owner team names. Publish a documentation build that excludes internal fields rather than relying on authors to be careful.

707. What is the correct way to handle a missing or invalid persisted query hash?

Return a stable, machine-readable error code such as PERSISTED_QUERY_NOT_FOUND so the client knows to retry with the full document or report a version mismatch. Do not fall back to executing arbitrary documents when strict mode is on. Log the hash and client version to drive rollout decisions.

708. What is the risk of unbounded variable size in a GraphQL request?

Variables bypass the usual argument-size intuition and can carry megabytes of nested JSON into a resolver or scalar parser. Enforce a maximum request body size at the edge and a per-variable depth and length limit. A JSON scalar accepting arbitrary depth is the most common offender.

709. How do you make error responses consistent enough for clients to handle programmatically?

Define a closed set of error codes in extensions.code, document their meaning, and keep messages stable and non-sensitive. Clients should branch on the code, never on message text. Version the code set as part of the API contract.

{ "errors": [{ "message": "Query too complex", "extensions": { "code": "QUERY_TOO_COMPLEX" } }] }

710. How do you redact PII in traces that capture resolver arguments automatically?

Configure the tracer to drop argument values entirely and record only types and lengths, or apply a directive-driven redaction at instrument time. Automatic instrumentation captures more than teams expect, including full input objects. Audit a sample of real traces before assuming redaction works.

711. What is the impact of a deep fragment spread combined with a high-cost field?

The fragment multiplies the expensive field across many parents, so cost grows as the product of spread count and field weight. Neither a fragment-count limit nor a per-field weight alone catches this. The cost model must compute expanded occurrences, not unique fields.

712. How do you enforce a maximum number of root fields in an operation?

Count root selections after fragment expansion and reject above a configured ceiling, which directly caps alias amplification at the top level. This is a cheap check that runs before any resolver. Combine it with per-level breadth caps for nested amplification.

713. Why is per-level breadth limiting different from depth limiting?

Breadth limiting bounds selections at each level, catching wide fans, while depth limiting bounds nesting, catching chains. A flat query with a thousand aliases is caught only by breadth, and a thousand-level chain only by depth. Mature gateways enforce both plus a total cost cap.

714. How do you avoid breaking legitimate clients when you introduce a cost limit?

Run the cost analyzer in report-only mode first, log the distribution of estimated cost by operation and client, and set the limit above the observed maximum of legitimate traffic. Then enforce, with a grace period that returns a warning header before hard rejection. Without the observation phase, the limit is a guess that breaks real users.

715. How do you monitor for a regression in query cost after a schema change?

Track the cost distribution of real operations per release and alert when the p95 or the max jumps beyond a threshold. A new expensive field or a changed default page size shows up immediately. Correlate with schema diffs so the responsible change is obvious.

716. What is the risk of computing total counts on every paginated field?

A count query often scans more than the page itself, so it can dominate cost while returning a single integer. Multiplied across nested lists it becomes quadratic. Make totals optional, cache them, or return an estimate with a separate field.

717. How do you protect against expensive sorting or filtering arguments?

Restrict sortable fields to an allowlist with supporting indexes, and reject combinations that cannot use an index. Unindexed sort on a large table is a classic accidental denial of service. Cost weights should reflect the difference between indexed and unindexed paths.

718. How do you prevent unbounded recursion inside a resolver, such as a tree walk?

Bound the traversal depth in code rather than relying on client-side query depth, since a resolver may expand a whole subtree internally. Use an iterative walk with an explicit limit and cycle detection on visited ids. Internal recursion is invisible to gateway-level limits.

719. How do you keep a per-tenant fair share when one tenant sends heavy traffic?

Apply weighted fair queueing or a token bucket per tenant so a heavy tenant cannot consume the whole concurrency budget. Enforce both a per-tenant cap and a global cap, with the per-tenant cap lower than the global one. Without fairness, one tenant's burst becomes everyone's outage.

720. What is the right behavior when the concurrency queue is full?

Reject immediately with a retry-after hint rather than accepting and queuing indefinitely. Queue time is latency the client cannot see, and unbounded queues convert a spike into a prolonged outage. Shedding early lets well-behaved clients retry with backoff.

721. How do you make retries safe for GraphQL mutations?

Every mutation that can be retried needs an idempotency key carried by the client and honored by the server. Without it, a network timeout after commit produces a duplicate on retry. Return the original result for a repeated key so the client sees success, not a conflict.

722. Why is a nonce different from an idempotency key?

An idempotency key makes repetition safe by returning the same result, while a nonce makes repetition impossible by being single-use. Nonces defend against replay of a captured request, whereas idempotency keys defend against accidental duplicate submission. High-risk mutations often need both.

723. How do you scope an idempotency key store so it does not grow without bound?

Store keys with a TTL matched to the client's maximum retry window, and partition by subject so lookups stay cheap. Purge by time rather than by count to keep the window predictable. Keys older than the retry window are useless and should expire automatically.

724. What breaks when two concurrent requests use the same idempotency key?

Both may pass the existence check and execute the side effect twice before either writes the key. The check-and-insert must be atomic, using a unique constraint or a lock. The second request should either wait for the first result or return a conflict.

725. How many subscriptions per connection is reasonable, and how do you enforce it?

Set a small cap based on the UI's realistic needs, enforced by counting active subscription ids per socket and rejecting new ones over the limit. Each subscription costs memory and often a backend listener, so the cap is a resource control, not a policy preference. Log rejections to distinguish abuse from a client bug.

726. How do you prevent a subscription from delivering events faster than the client can consume them?

Use a bounded per-connection queue and drop or coalesce events when it fills, then disconnect if it stays full. Coalescing is appropriate for state snapshots, while dropping loses information. Either way, unbounded buffering is not an option.

727. How do you attribute latency to the gateway versus the resolvers?

Measure time from request receipt to execution start, then the sum of resolver spans, then serialization time. A large gap before execution points at parsing, validation, or queueing; a large serialization time points at response size. Without these three buckets, the bottleneck is unattributable.

728. Why is validation cost itself worth measuring?

Large documents with many fragments can spend significant CPU in parsing and validation before any resolver runs, which is exactly where a cheap attack lands. Track validation duration and document size per operation. A cost limit that only counts resolvers misses this entirely.

729. How do you prevent validation from becoming the denial-of-service vector?

Cap document size and token count before parsing, cache validation results by document hash, and use a parser with linear behavior. Rejecting a two-megabyte document at the edge costs almost nothing. Caching validated documents also removes repeated validation work for persisted queries.

730. How do you test cost and depth limits without shipping a bypass to production?

Run the enforcement in unit tests against the analyzer with fixture documents, and in staging with the real gateway configuration. Never add a header-based bypass in production code, because it becomes the vulnerability. If you need production verification, use report-only mode that logs rather than skips.

731. What is a reasonable way to communicate a rejected over-budget query to a client developer?

Return a stable code, the estimated cost, the applicable limit, and a hint about which field or argument dominated the estimate. Developer-facing detail dramatically shortens the fix cycle. Keep it out of responses to unauthenticated or untrusted clients if the detail reveals schema internals.

{ "code": "QUERY_TOO_COMPLEX", "cost": 8420, "limit": 5000, "hint": "users.first" }

732. How does per-request context construction itself become a performance bottleneck?

Context construction often performs token verification, permission loading, and tenant lookup on every request, which can dominate latency for cheap queries. Cache verification keys and permission sets with a short TTL keyed on the token's subject and claims version. Measure context time separately from resolver time to see it at all.

733. How do you keep authorization checks from issuing an N+1 query per field?

Load the subject's permission set once per request and evaluate decisions in memory, or batch permission lookups through a loader keyed by resource. Per-field database checks multiply latency exactly like any other N+1. Cache the decision for the duration of the operation.

734. What do you check to confirm tenant isolation actually holds under load?

Run concurrent multi-tenant tests that assert every response contains only the requesting tenant's data, including under batching and caching. Load conditions expose races that single-threaded tests miss. Add a canary tenant whose data must never appear elsewhere.

735. What is the correct ordering of admission checks for an incoming operation?

Body size, then transport and auth, then parse, then validation including depth, breadth, and fragment limits, then cost estimation, then rate and concurrency admission, then execution. Cheapest checks first, because expensive checks are themselves an attack surface. Authorization of individual fields happens during execution.

736. Why should authentication precede parsing?

Parsing untrusted input costs CPU, so rejecting unauthenticated requests before parsing removes free work from attackers. It also means unauthenticated traffic cannot probe the parser for crashes. Only the body-size check needs to come earlier.

737. How do you keep cost estimation fast enough to run on every request?

Cache the estimate by document hash so repeated persisted queries skip analysis, and keep the analyzer a single linear pass over the validated document. Avoid re-expanding fragments per occurrence. If estimation itself is slow, it becomes the bottleneck under load.

738. What is the role of a query planner in a federated GraphQL gateway from a security standpoint?

The planner decides which subgraphs receive which fields, so a planning bug can send a query to a subgraph that lacks the authorization context. Every subgraph call must carry the original subject and tenant, not only the fields. Trust boundaries inside the graph need the same checks as the edge.

739. How do you ensure subgraph calls cannot be replayed by another tenant?

Sign subgraph requests with a gateway-held key that includes the tenant and subject, and have subgraphs reject unsigned or mismatched calls. Network-level trust alone fails once anything inside the mesh is compromised. Short-lived signatures limit the replay window.

740. What does "fail closed" mean for a GraphQL security control?

If the cost analyzer, policy engine, or allowlist store is unavailable, the request is rejected rather than allowed through unchecked. The common failure is a try/catch that logs and proceeds, which turns an outage into a bypass. Health checks should treat a degraded control as not ready.

741. Why is measuring p95 per operation more actionable than a single API-wide p95?

An API-wide percentile blends cheap and expensive operations, so it moves slowly and points nowhere. Per-operation percentiles show which query shape regressed and let you compare against that operation's own baseline. Aggregate numbers are for dashboards, per-operation numbers are for debugging.

742. What is label cardinality risk in GraphQL metrics?

Labeling metrics with raw query text, operation names, or user ids creates unbounded series and can take down the metrics backend. Use a bounded fingerprint set and a separate high-cardinality store for detailed analysis. Cardinality is a cost and availability concern, not just tidiness.

743. How do you decide the window for rate limiting expensive operations?

Match the window to the business control you need: a short window smooths bursts, a long window enforces a daily quota. Token buckets with a burst allowance behave better than fixed windows, which permit double bursts at boundaries. Document the effective burst, not just the average rate.

744. How do you protect a GraphQL endpoint that must remain publicly readable without auth?

Enforce strict cost and depth limits, require persisted queries, cap page sizes, and rate limit by IP and by a client-provided but validated API key. Public read access still needs admission control, because the cost of a read is not zero. Add a CDN for cacheable GET queries to absorb the bulk.

745. What is the risk of a permissive JSON scalar in a public schema?

It accepts arbitrary structure, so depth, size, and type validation are all bypassed, and downstream consumers may trust it blindly. Bound the depth and byte size on input and validate the shape against a schema. A permissive scalar is effectively an unvalidated API inside the API.

746. What is the operational value of a per-operation error budget?

It distinguishes a systemic regression from a single client's malformed traffic, because errors are attributed to an operation shape and a client version. Alerts then point at a deploy or a client release. Without attribution, every error spike looks the same.

747. Why does schema federation exist as an alternative to a single GraphQL monolith?

Federation exists because a single schema owned by one team cannot scale organizationally once many teams need to contribute fields over the same domain graph. It lets each team own a subgraph with its own deploy cadence, runtime, and data stores while clients still see one composed supergraph. The trade-off is that composition, query planning, and governance become first-class platform problems rather than being solved by a single codebase.

748. What are the practical limits of a monolithic GraphQL schema in a large organization?

The monolith couples release cadence, so every team ships through one pipeline and one blast radius. Ownership blurs because anyone can add a field to any type, which makes review and accountability weak. Scaling is also coarse: you scale the whole server for one hot resolver, and a single bad field can degrade the entire graph.

749. How does an Apollo Federation v2 subgraph differ from a standalone GraphQL server?

A subgraph is a normal GraphQL server that additionally understands federation directives and can resolve entity references for types it declares with @key. It exposes federation-specific metadata through the _service field so the composer can introspect its SDL. Operationally it can still be run and tested independently, but its schema is only a partial view of the client-facing API.

750. What does @key actually declare, and what are its runtime obligations?

@key declares that a type is an entity and names the field set that uniquely identifies an instance of it across subgraphs. The declaring subgraph must be able to resolve that entity by reference, typically through a reference resolver keyed on those fields. The key fields must be resolvable without external data and must be stable, because the router uses them to stitch results together.

type Product @key(fields: "id") {
  id: ID!
  name: String!
}

751. How does entity resolution work when the router needs a field from another subgraph?

The router receives representations, which are objects containing only the key fields, and sends them to the owning subgraph via a _entities query. That subgraph runs its reference resolver to hydrate each representation into a full object. The router then merges the returned fields into the response it is assembling for the client.

752. How do you implement __resolveReference in a JavaScript subgraph, and what are common mistakes?

The resolver receives the representation object and must return the entity, typically via a dataloader or batch lookup keyed on the representation fields. Common mistakes are doing a per-representation database call, ignoring the possibility of a missing entity, and returning a partially populated object that violates non-null fields in the subgraph schema.

Product: {
  __resolveReference: (ref, { loaders }) => loaders.product.load(ref.id),
}

753. Why is batching in reference resolvers important for federated performance?

The router sends many representations in a single _entities call, so a naive resolver turns one request into N database round trips. Batching collapses those into one query, which matters most for list fields where a single parent list fans out to hundreds of entities. Without batching, latency grows linearly with list size and the subgraph becomes the bottleneck.

754. What is the difference between an entity and a value type in federation?

An entity has a stable identity defined by @key and can be extended and referenced across subgraphs. A value type has no cross-subgraph identity, so it must be defined consistently and is generally only usable within the subgraph that defines it. Choosing entity versus value type is an architectural decision about whether other teams need to attach fields to that object.

755. When should a type be marked @shareable?

@shareable declares that multiple subgraphs may resolve the same field and that their values are expected to agree. It is appropriate for stable, low-volatility data such as a display name or a status enum that several domains legitimately need to serve. It is dangerous when the field is derived from different sources, because readers will see inconsistent values depending on which subgraph the planner picks.

type Product @key(fields: "id") {
  id: ID!
  name: String! @shareable
}

756. What breaks when a field is declared in two subgraphs without @shareable?

Composition fails with a conflict error because the composer cannot decide which subgraph is authoritative for that field. This is intentional: silent resolution would hide genuine ownership ambiguity. The fix is either to make one subgraph authoritative and mark the others @external, or to explicitly declare the field @shareable after confirming the values agree.

757. Explain @external and when it is still required in Federation v2.

@external marks a field that a subgraph references but does not own, so the subgraph cannot resolve it on its own. In v2 it is mainly needed when a field is used in @requires or when providing a reference to a type owned elsewhere. It is a correctness marker, not documentation, and composition will reject @requires on a field that is not declared external or locally defined.

758. How does @requires work, and what is the performance consequence?

@requires lets a subgraph compute a field from data owned by another subgraph, by declaring the external fields it needs as inputs. The router must first fetch those fields from the owning subgraph, then send them in the representation to the computing subgraph. This adds a plan step and increases payload size, so it should be used sparingly and never for large blobs.

type Product @key(fields: "id") {
  id: ID!
  weight: Float @external
  shippingCost: Float @requires(fields: "weight")
}

759. What problem does @provides solve, and how can it be misused?

@provides tells the router that a subgraph can return certain fields of a referenced entity even though another subgraph owns them, avoiding an extra fetch. It is useful when the subgraph already has a denormalized copy of that data. It is misused when the provided data is stale or partial, because the router trusts the declaration and will not re-fetch from the owner.

760. How would you use @override to migrate a field from one subgraph to another?

Add the field to the new subgraph with @override(from: "OldSubgraph"), deploy it, and verify behavior before removing the definition from the old subgraph. Composition then routes the field exclusively to the new owner while the old definition still exists as a fallback during rollout. Once traffic is confirmed healthy, delete the old field and the override annotation in a follow-up change.

type Product @key(fields: "id") {
  id: ID!
  price: Money @override(from: "pricing")
}

761. What is progressive @override with a label, and why is it safer than a hard cutover?

A labeled override lets you roll a percentage of traffic to the new field owner and monitor error rates before full migration. Without it, a hard cutover exposes every client to a new code path at once, including its latency and correctness risks. The label is removed once the rollout reaches a hundred percent, leaving the field as the sole owner.

762. What is @interfaceObject and which migration problem does it address?

@interfaceObject lets a subgraph contribute fields to an entity interface without owning any of its concrete implementations. It solves the case where a second team wants to add a field to every entity behind an interface, such as a review count, without importing every implementing type. The router resolves the field against the interface object and applies it across implementations at query time.

763. How does @inaccessible help control the client-facing API surface?

@inaccessible keeps an element in the composed supergraph for internal routing but removes it from the public API schema served to clients. It is useful for fields that exist only to support entity resolution or internal migration scaffolding. Because the element disappears from introspection and validation, clients cannot depend on it accidentally.

764. When should you use @inaccessible instead of simply not exposing a field?

You need it when the field must exist in the supergraph for composition, such as a key field used only for cross-subgraph joins. If the field has no routing role, omit it entirely rather than hiding it. Hiding is a tool for internal plumbing, not a general substitute for schema hygiene.

765. How do @tag and contracts produce different API variants from one supergraph?

@tag annotates schema elements with labels such as @tag(name: "public"), and a contract filters the supergraph down to elements carrying an allowed set of tags. Each contract is published as its own schema and served by its own router instance. This lets partners see a smaller, stable surface while internal clients see everything, without maintaining separate hand-written schemas.

766. What are the governance risks of contracts built on @tag?

Tags are additive metadata, so a forgotten tag can silently expose a field to a contract it was never meant for, or hide one a consumer needs. There is no type-level guarantee that a contract remains valid after refactors, so contract validity must be tested in CI. Teams should treat tag assignment as a reviewed, owned decision rather than an afterthought.

767. What is the difference between federation directives and ordinary custom directives?

Federation directives are interpreted by the composer and the router and change how the supergraph is built or how queries are planned. Custom directives are application-level metadata that the runtime may ignore entirely. Marking something with a custom directive never affects routing, and expecting it to is a common source of confusion.

768. Why can't a custom directive be used to express ownership across subgraphs?

Ownership is a composition concern that the router must enforce at plan time, so it requires a directive the composer understands. A custom directive would be invisible to composition, leaving two subgraphs both claiming a field with no conflict detection. Use @shareable, @external, or @override for ownership semantics and reserve custom directives for documentation or codegen.

769. What are the core composition rules a subgraph must satisfy to join a supergraph?

Every entity key must be resolvable in the subgraph that declares it, and shared types must agree on kind, nullability, and field types. Conflicting ownership requires @shareable, @external, or @override, and directives must be valid for their location. Composition also enforces that referenced types exist and that arguments on shared fields match.

770. What typically causes a composition failure after an otherwise valid subgraph change?

The most common causes are nullability drift on a shared field, changing a field's type on one side only, and removing a type still referenced by another subgraph. Adding a required argument to a shared field also breaks composition because other subgraphs do not supply it. Reading the composition error's path and hint usually identifies the offending subgraph immediately.

771. What is the supergraph schema, and who consumes it?

The supergraph schema is the composed artifact containing every subgraph's types plus the join metadata describing which subgraph resolves what. It is consumed by the router, not by clients, and it is the input to query planning. Clients see a derived API schema that strips federation internals.

772. How does the router turn a client operation into a plan across subgraphs?

The router parses and validates the operation against the API schema, then builds a query plan by walking the selection set and consulting join metadata to find the owning subgraph for each field. It groups fields into fetch nodes and inserts flatten steps where entity keys must be sent to another subgraph. The plan is then executed, often with parallel fetches where dependencies allow.

773. What does a fetch node with a "representations" variable tell you about a plan?

It indicates an entity join: the router is sending key objects to another subgraph to retrieve fields it does not own. The size of that representations array is the fan-out you need to watch, because it directly drives the subgraph's batch size. Large arrays usually mean the plan is doing per-item resolution on a big parent list.

774. How would you diagnose a query that produces an unexpectedly chatty plan?

Start with the query plan output from the router's dry-run or debug tooling and count fetch nodes and their sequence depth. Then check whether shared fields were declared without @shareable, which forces joins, or whether @provides could eliminate a fetch. Tracing per subgraph span durations usually confirms which node dominates the latency.

775. What is a plan explosion, and what schema shapes cause it?

A plan explosion is a combinatorial growth in fetch nodes, often from nested entity joins across several subgraphs or from interfaces with many implementations. Deeply nested lists that each require an entity fetch multiply the number of round trips. Cycles of @requires and over-granular subgraph splits make it much worse.

776. What concrete steps reduce plan explosion once it is identified?

Consolidate tightly coupled fields into the same subgraph so a join disappears, and add @provides where a subgraph can supply data it already holds. Flatten nested entity fetches by resolving intermediate data in the owning subgraph, and use @interfaceObject to avoid per-implementation joins. Finally, cap query depth and complexity at the router so pathological operations are rejected rather than planned.

777. Why does query depth limiting matter more in a federated graph than in a monolith?

Each additional depth level can cross a subgraph boundary and multiply fan-out, so cost grows faster than in a single-process resolver chain. A deep query in a monolith is one expensive database call; in a supergraph it can be dozens of coordinated round trips. Depth and complexity limits protect both the router and the subgraphs from that amplification.

778. How do schema checks fit into a CI pipeline for subgraph changes?

CI composes the proposed subgraph schema against the current published supergraph and fails the build on composition errors or breaking changes. The check runs before merge so the author gets feedback in the pull request rather than after deploy. Publishing is then a separate, gated step that only runs on the main branch.

rover subgraph check my-graph@current \
  --name products --schema ./schema.graphql

779. What counts as a breaking change in a federated schema check?

Removing a field or type, tightening nullability, removing an enum value, or changing a field's type are the classic breaking changes for clients. Adding a required argument is breaking for existing queries even though the schema still composes. Schema checks compare against real recorded operations when available, which catches usage-dependent breaks that pure SDL diffing misses.

780. Why is operation-aware breaking change detection more reliable than SDL diffing?

SDL diffing only knows what the schema declares, so it cannot tell whether a risky change is actually used by any client. Operation-aware checks compare the change against real recorded operation bodies and flag breaks only for queries that genuinely select the affected field or argument. This reduces false alarms on dead fields and catches subtle breaks like argument coercion that a schema diff would miss.

781. Describe a healthy schema registry workflow from local development to production.

Developers run a local composition check, push a variant branch, and let CI run a schema check against the latest production variant. On merge, the subgraph is published to a non-production variant and validated with smoke tests, then promoted to production after router validation. The registry keeps the launch history so any supergraph can be rolled back to a known-good composition.

782. What are schema variants, and why do teams use them?

A variant is an isolated copy of the graph's composition history, usually mapped to an environment such as staging or production. Subgraphs publish into a variant, and the composition of that variant is what its routers fetch. Variants let teams test schema changes end-to-end without touching the production graph.

783. How do you handle a subgraph that must ship before the router is upgraded?

Pin the router to a version that supports the federation features the subgraph uses, and verify compatibility in staging before publishing. If the router lags, either delay the subgraph feature or gate it behind an @inaccessible field until the upgrade lands. Router and subgraph versions should be treated as one compatibility matrix, not independent tracks.

784. What does contract testing mean between subgraphs?

Contract testing verifies that the assumptions one subgraph makes about another still hold, such as the existence of a key field or the shape of a provided value. It typically runs against a shared schema fixture or a mock subgraph in CI. The goal is to catch cross-team breakage before it reaches composition in production.

785. How would you test a subgraph in isolation without the full supergraph?

Run the subgraph as a standalone GraphQL server and query it directly for its own fields, plus test _entities with hand-written representations. For fields that depend on external data, stub the reference resolver inputs. This catches resolver and nullability bugs quickly, though it cannot validate plan behavior across boundaries.

query ($reps: [_Any!]!) {
  _entities(representations: $reps) { ... on Product { name } }
}

786. What should a supergraph-level integration test cover that a subgraph test cannot?

It should cover operations that span subgraph boundaries, verifying that entity joins return complete and correctly merged objects. It should also assert that partial failures surface as expected errors with the right paths. Response-shape regression tests on key client operations are the most valuable part of this layer.

787. What versioning strategy works for a supergraph that many clients depend on?

Prefer continuous, additive evolution over numbered supergraph versions, because a supergraph is a composition of independently versioned subgraphs. Use deprecation plus usage data to retire fields rather than cutting a v2. Where a hard break is unavoidable, publish it as a tagged contract and migrate consumers explicitly.

788. Why is URL or header-based supergraph versioning usually a mistake?

It forks the graph so every fix must be applied to multiple variants, and entity joins across versions become ambiguous. Clients also tend to pin indefinitely, which freezes the schema rather than enabling migration. Additive evolution with deprecation keeps one graph and one source of truth.

789. How do you safely deprecate a field that exists in several subgraphs?

Mark it @deprecated with a reason and a migration target, then watch usage per client until it reaches zero. Only then remove it, and remove the definition in every subgraph that declared it in the same release to avoid composition conflicts. Deprecation must precede removal by enough time for mobile clients to ship an update.

790. What does the removal of a field require when it is part of an entity key?

Key fields cannot be deprecated away casually, because the router depends on them to join entities. Removing one usually requires introducing a replacement key, migrating all referencing subgraphs, and only then dropping the old key. This is effectively a schema-surgery project with a staged rollout, not a routine deletion.

791. How would you run a canary rollout of a schema change at the router layer?

Deploy the new supergraph to a small percentage of router instances or a fraction of traffic, and compare error rate and latency against the baseline. Watch per-subgraph spans and plan shapes, since a change can be correct but much more expensive. Promote only after the canary holds through a representative traffic window.

792. What signals tell you a canary schema change should be rolled back?

A rise in GraphQL errors with non-null propagation, an increase in p99 latency, or a jump in subgraph error rates are immediate rollback signals. A change in plan shape toward more fetch nodes is a slower-burning warning. Rollback should be a single registry operation that republishes the previous supergraph, not a code revert.

793. What does operation-level tracing add over per-request metrics in a federated graph?

It attributes time and errors to individual subgraph fetches within a single operation, revealing which node in the plan is slow. Aggregated metrics hide whether latency comes from the router, one hot subgraph, or serialization between them. Traces also expose the plan shape, which is essential for diagnosing pathological queries.

794. Which dimensions should federated traces carry to be useful for platform debugging?

Include the operation name and hash, the client identifier, the subgraph name, the fetch node label, and whether the response was partial. Sampling should be biased toward errors and slow operations so rare failures are not lost. Without the operation hash, correlating a trace to a persisted query or a schema check is painful.

795. How should a router propagate errors from subgraphs to clients?

It should preserve the subgraph error's message, path, and extensions while adding context about which service produced it. Non-null violations inside a subgraph should null out only the affected branch, not the whole response. The router must avoid swallowing errors, because silent nulls are the hardest federated bugs to diagnose.

796. What is partial data in federation, and how do clients know they received it?

Partial data occurs when one subgraph fails while others succeed, so the response contains valid data plus an errors array describing the failed paths. Clients must inspect both data and errors rather than treating a non-null data as full success. Well-designed clients render the available fields and degrade the missing section explicitly.

797. Why can one subgraph failure null out an entire parent object?

If the failed field is non-null and its parent is also non-null, GraphQL's error propagation bubbles up until it reaches a nullable boundary. In a federated plan, that boundary may be several levels above where the failure occurred. This is why nullability choices in subgraphs have graph-wide consequences.

798. How does authentication propagate from the router to subgraphs?

The router typically validates the client token once and forwards either the original token or a derived, scoped token to each subgraph via headers. Subgraphs should verify the token themselves rather than trusting the router blindly, especially if they are reachable internally. The forwarding contract must be explicit so a new subgraph does not silently lack credentials.

799. What is the risk of forwarding the raw client token to every subgraph?

It gives every subgraph full access to the client's broadest privileges, violating least privilege across service boundaries. A compromised or over-broad subgraph can then act with the user's full authority. Deriving per-subgraph scoped tokens limits the blast radius and makes audit trails more meaningful.

800. Where should authorization live in a federated architecture?

Enforcement belongs in the owning subgraph, because only it knows the data-level rules for its domain. The router can perform coarse gates such as requiring authentication or blocking entire operations, but it should not be the sole authorization layer. Defense in depth means the router limits exposure and each subgraph enforces its own policy.

801. Name a per-subgraph authorization pitfall that only appears in federation.

A subgraph may authorize based on the fields requested in its own fetch node, not realizing the same entity was fetched elsewhere with different fields. Another pitfall is trusting a representation's key fields without checking that the caller is allowed to see that entity at all. Both produce leaks that are invisible in single-service testing.

802. How do you prevent field-level leaks when a subgraph returns data for an entity fetched by another subgraph?

Apply the same field-level policy in the reference resolver path as in the root query path, since both can return the same object. Treat _entities as a first-class entry point that needs the caller's identity and authorization context. Tests should exercise entity fetches with low-privilege tokens, not just root queries.

803. What changes when a federated graph must serve multiple tenants?

Tenant context must flow from the router through every subgraph fetch, usually as a verified claim in a propagated header or token. Entity keys must be tenant-safe, because a key that is only unique per tenant can collide across tenants during joins. Caching at the router must be partitioned by tenant to avoid cross-tenant leakage.

804. Why are globally unique entity IDs important in a multi-tenant graph?

The router treats an entity key as globally identifying, so two tenants sharing a numeric ID would be merged into one object during a join. Encode the tenant in the key or use globally unique identifiers. This is a data modeling constraint that must be enforced at the database level, not just in resolvers.

805. What are the main caching layers available at a federation router?

The router can cache full query responses keyed by operation plus variables and identity, or cache entity representations for repeated joins. It may also cache introspection and persisted query manifests. Each layer needs an invalidation story tied to subgraph mutations, otherwise stale data becomes the default.

806. Why is response caching at the router harder than at a single GraphQL server?

A single response may combine data from several subgraphs with different freshness and different authorization scopes. Any mutation in one domain can invalidate a cached response that touched it, and the router has no automatic knowledge of that dependency. Most teams therefore cache conservatively, or cache at the subgraph and entity level instead.

807. How do persisted queries change router operations and security?

Clients send a hash instead of a query body, so the router looks up the operation from an allowlist and rejects unknown documents. This shrinks payloads, blocks arbitrary query injection, and makes plan caching more effective. It requires a build-time manifest pipeline and a strategy for safe rollout of new operations.

808. What breaks when a persisted query manifest is deployed out of sync with clients?

Clients sending a hash the router does not recognize receive a validation error, which surfaces as a hard failure for that feature. This is common when a web deploy and the manifest deploy race, or when an old mobile build calls a retired operation. Keeping retired hashes valid for a deprecation window avoids breaking long-lived clients.

809. Should rate limiting live at the router, in subgraphs, or both?

Both, with different purposes: the router enforces per-client and per-operation limits based on cost, while subgraphs protect their own resources from any caller including the router. Router-only limiting fails when internal callers bypass it; subgraph-only limiting cannot distinguish abusive clients from legitimate fan-out. Cost-aware limits at the router plus concurrency limits per subgraph is a sound default.

810. How would you rate limit by query cost rather than by request count?

Compute a static cost from the operation using field weights and list multipliers, then reject operations above a threshold or deduct from a per-client budget. Cost-based limiting is fairer because one expensive query can equal hundreds of cheap ones. The weights need periodic review as the schema grows.

811. Compare federation, schema stitching, and a backend-for-frontend approach.

Stitching merges remote schemas at runtime with hand-written merge logic and tends to accumulate per-service glue. Federation moves the contract into the schema via directives, so composition and planning are automated and ownership is explicit. A BFF is a single-team facade tailored to one client, which is simpler but duplicates logic once several clients need the same graph.

812. What makes migrating from stitching to federation risky?

Stitching often encodes merge behavior in code that has no federation equivalent, such as custom type merging or field-level resolvers in the gateway. Those behaviors must be re-expressed as keys, shareable fields, or @requires, which can change semantics. A staged migration with both gateways running against the same subgraphs lets you diff responses before cutting over.

813. What is a practical first step when migrating a stitched gateway to federation?

Start by identifying the entity types that actually cross service boundaries and give them stable keys, since those are the only types that need to be federated. Then convert one subgraph at a time and run response-diff tests against the existing gateway. Types that never cross boundaries can stay as plain value types.

814. When is federation overkill for an organization?

Federation is overkill when one team owns the whole graph and can ship changes in a single deploy, or when the domain has few cross-cutting entities. The operational cost of a router, registry, composition pipeline, and multi-repo coordination outweighs the autonomy gained. A modular monolith schema with clear internal module boundaries delivers most of the benefit without the platform burden.

815. How would you structure a modular monolith GraphQL schema for future federation?

Split the schema by domain module with explicit boundaries, and forbid modules from reaching into each other's resolvers directly. Give cross-module entities stable identifiers now, so they can later become keys without a data migration. Keep the schema assembly in one place so extracting a module into a subgraph is a packaging change, not a redesign.

816. How do you decide where to draw subgraph boundaries?

Boundaries should follow domain ownership and data ownership, so a team can change its subgraph without coordinating a cross-team release. Types that change together and are queried together usually belong together, because splitting them creates joins. A useful test is whether the boundary reduces coupling rather than just redistributing it.

817. What goes wrong when subgraphs are split by technical layer instead of domain?

A layer split such as a "read model" subgraph and a "search" subgraph forces entity joins for almost every query, producing chatty plans. Ownership also becomes ambiguous because both layers touch the same entities. Domain-aligned subgraphs keep most operations resolvable within one service.

818. What naming and namespace conventions help across many subgraphs?

Use domain-prefixed type names where collisions are likely, consistent casing for fields, and a single convention for ID versus slug identifiers. Reserve a per-domain prefix for enums because enum value collisions across subgraphs are a frequent composition failure. Document the conventions as lint rules rather than prose, so they are enforced automatically.

819. Why do enum values collide so often in federated schemas?

Enums are composed by name, so two subgraphs defining Status with different values either conflict or silently merge into a union that no single subgraph can serve. Prefixing values or namespacing enum type names avoids the ambiguity. Composition errors here are often the first sign that two teams modeled the same concept differently.

820. Which schema lint rules would you enforce in a federated codebase?

Enforce entity key stability, require descriptions on public fields, ban deprecated fields in new code, and require explicit nullability reviews. Add custom rules for naming conventions and for forbidding direct database types leaking into the API. Lint should fail the build on violations rather than warn, or it will be ignored.

821. How do you generate useful documentation for a supergraph rather than per-subgraph docs?

Generate from the composed API schema, not from individual subgraph SDL, so consumers see the real surface they can query. Include ownership metadata per field, deprecation notices, and example operations drawn from recorded traffic. Publishing docs from the registry keeps them in sync with the deployed supergraph automatically.

822. How should the schema be designed for very different clients such as web, mobile, and partners?

Keep a single coherent graph and let clients select what they need, rather than forking per client. Where payload weight matters for mobile, provide pagination and field-level granularity instead of separate schemas. Partners usually need a narrower, more stable contract, which is a good fit for a tagged contract rather than a fork.

823. What is the risk of building a separate schema per client team?

Each fork duplicates resolvers and drifts semantically, so a bug fix must be applied several times and behavior diverges. Clients then disagree about what a field means, which is worse than a slightly heavier payload. Contracts and field-level selection solve the same need without forking the graph.

824. What should an API contract with an external consumer guarantee?

It should guarantee the fields and semantics the consumer depends on, a deprecation notice period, and a defined process for breaking changes. External contracts also need rate limits, authentication expectations, and a support channel documented alongside the schema. Vague contracts turn every schema change into a negotiation.

825. How do you run a schema RFC process without slowing delivery to a crawl?

Reserve full RFCs for cross-subgraph changes, breaking changes, and new shared types, and let local additive changes go through normal review. Set a short review window with a named approver per domain so proposals do not stall. Publish decisions in the registry so future teams can see why the schema looks the way it does.

826. What review gates should a schema change pass before reaching production?

Composition must succeed, schema checks must report no breaking changes for used operations, and lint rules must pass. A domain owner should review changes that affect shared types or entity keys. For high-risk changes, require an explicit rollout plan with a rollback path.

827. What does a sunset policy for a field need to specify?

It needs the deprecation date, the removal date, the migration path, and the contact for exceptions. It should also define how usage is measured, because "nobody uses it" is only credible with per-client telemetry. Publishing the policy alongside the deprecation reason makes it discoverable at the point of use.

828. How do you measure whether a supergraph is healthy over time?

Track the ratio of used to unused fields, the volume of traffic on deprecated fields, and the count of clients still on legacy operations. Composition failure rate and time-to-fix are good indicators of governance health. Error rate and p99 latency per subgraph complete the picture.

829. How would you find fields that no client has queried in ninety days?

Use registry field-usage telemetry keyed by operation and client, then aggregate by coordinate over the window. Care must be taken with low-frequency administrative fields, which may be legitimately rare. Treat the result as candidates for deprecation, confirmed by owner review rather than automatic deletion.

830. What is field cost accounting, and how does it enable chargeback?

Assign each field or resolver a cost representing its compute or data expense, then attribute consumed cost to the calling client or team. This makes expensive fields visible and lets platform teams bill or budget by usage. The model only works if costs are periodically recalibrated against real subgraph timings.

831. How do you prevent one team's expensive field from degrading the shared router?

Set per-subgraph concurrency and timeout limits at the router so a slow domain cannot consume all capacity. Add cost-based admission control for operations dominated by expensive fields, and alert on per-subgraph latency budgets. Chargeback or cost visibility gives the owning team an incentive to optimize.

832. What happens to a federated query when one subgraph times out?

The router records an error for that fetch node's paths and returns whatever the other subgraphs produced, subject to null propagation. If the failed field was non-null, the affected branch collapses. Timeouts should be set per subgraph, with a graph-level deadline to bound total latency.

833. Why is a global request deadline important in a federated router?

Without it, serial plan steps can each consume their own timeout, so total latency becomes the sum of many slow hops. A deadline lets the router abandon remaining work and return partial data within a predictable bound. It also protects subgraphs from work whose results nobody will read.

834. How does the router stay stateless enough to scale horizontally?

The router holds only the supergraph schema and its compiled plan cache in memory, so any instance can serve any request. Session or identity state is carried in tokens rather than in process memory. That lets instances be replaced freely, though it means plan cache warmup and config propagation become the operational concerns.

835. What has to be reloaded when a new supergraph is published?

Every router instance must fetch the new supergraph artifact and rebuild its validator and planner. In-flight requests usually continue on the old schema while new ones use the new one. Staggered reloads and health checks prevent a thundering herd against the registry.

836. What does @key with resolvable: false mean, and when is it useful?

It declares the entity and its key for composition without requiring this subgraph to implement a reference resolver. That suits subgraphs that can produce the entity as a return value but never need to be asked for it by key. It avoids dead _entities code while still letting the type participate in joins.

type Review @key(fields: "id", resolvable: false) {
  id: ID!
}

837. Can an entity declare more than one @key, and what problem does that solve?

Yes, and each key becomes an alternative way for the router to fetch that entity. Multiple keys help when some subgraphs know only a slug and others know a database ID, avoiding an extra lookup hop. Each key must be independently resolvable in the declaring subgraph.

838. How do nested key fields behave in a federated join?

A key such as @key(fields: "sku package { id }") puts the whole nested object into the representation. The reference resolver receives that structure and must handle it exactly, including nested nullability. Nested keys increase payload size, so they should be used only when a flat identifier is genuinely unavailable.

839. How do you federate a legacy REST service that exposes no GraphQL at all?

Write a thin subgraph that wraps the REST calls in resolvers and declares entity keys for the objects other teams need to extend. The wrapper isolates the REST quirks, including pagination and error shapes, from the rest of the graph. Keep the wrapper narrow so it does not become a second, unowned API layer.

840. What are the trade-offs of wrapping a legacy API as a subgraph?

You gain a unified graph and can evolve the client contract independently of the legacy service. You inherit the legacy latency, error semantics, and rate limits, which surface as federated errors you cannot fix locally. If the legacy service is slow and chatty, the wrapper needs caching and batching to avoid poisoning plan performance.

841. How should a mutation that must touch two subgraphs be modeled?

Model it as a single mutation in the subgraph that owns the workflow, and have that subgraph call the other service internally rather than splitting the mutation across two subgraphs. The client-facing operation should be atomic from the caller's perspective. Splitting it into two client mutations pushes consistency handling onto every consumer.

842. Why is a distributed transaction across subgraphs usually the wrong answer?

Two-phase commit across independent services couples their availability and adds coordination latency to every write. Federation has no notion of a transaction spanning subgraphs, so you would be building one outside the graph anyway. Sagas or compensating actions with an explicit state machine are more resilient.

843. How do subscriptions work in a federated graph?

Subscriptions are served by the subgraph that owns the event stream, and the router proxies the connection and applies the plan for the selected fields. Fields from other subgraphs are resolved per event, which can multiply cost on high-frequency streams. Many teams restrict subscription payloads to fields owned by the emitting subgraph to keep that cost bounded.

844. How does @defer interact with entity joins in a plan?

Deferred fragments are planned as separate fetch groups, so the router can return the initial payload before the deferred part completes. When a deferred fragment requires a cross-subgraph join, that join happens after the initial response, adding a second round of latency. Defer improves perceived performance but not the underlying work.

845. What goes wrong with cursor pagination when a list is assembled across subgraphs?

Cursors are usually opaque and owned by one subgraph, so the router cannot paginate a merged list consistently across two sources. Ordering also becomes ambiguous once results are combined. The practical rule is that paginated lists should be resolved within a single subgraph.

846. How do you keep pagination cursors stable across subgraph deployments?

Encode cursors as stable sort keys plus a tiebreaker rather than offsets or database row positions. Never change the sort definition without versioning the cursor, or old cursors will silently return wrong pages. Document the cursor contract so refactors do not break long-lived client pagination.

847. How would you split one large subgraph into two without downtime?

First extract the entities and fields into a new subgraph while the original still serves them, using @override to shift ownership gradually. Verify with response-diff tests and per-subgraph traces, then delete the moved definitions from the original. Never remove both definitions in the same step, or composition will fail mid-rollout.

848. What do you check before merging two subgraphs that overlap heavily?

Confirm that shared fields are semantically identical, not merely the same type, since merging hides behavioral differences. Resolve duplicate key definitions and reconcile @shareable annotations before composition. Expect to lose independent deploy cadence, which is the main reason the split existed.

849. How do you retire an entire subgraph?

Move every field it uniquely owns to another owner or deprecate it, and confirm no plan still routes to that subgraph using traces and plan inspection. Remove it from the composition input only after the supergraph no longer references it. Keeping the router pointed at a decommissioned URL is a common cause of partial-data incidents.

850. What is the correct promotion path for a subgraph change across variants?

Publish to a development variant, run integration tests against a staging router, and then promote the same artifact to production. Promotion should move an immutable build, not rebuild from a branch, so what was tested is what ships. Contract and composition checks should run at each hop.

851. Why should router configuration live in version control?

Router config carries routing, header propagation, timeouts, and traffic shaping, all of which affect correctness and security. Version control gives review, rollback, and an audit trail for changes that are otherwise invisible. GitOps-style reconciliation also prevents drift between environments.

852. What belongs in router configuration rather than in the supergraph schema?

Transport concerns belong in config: timeouts, header propagation rules, TLS, rate limits, and telemetry exporters. Schema shape belongs in the supergraph, because it must be composed from subgraphs and validated for consistency. Putting routing behavior into schema directives blurs that line and makes changes harder to review.

853. Why must custom scalars behave identically across subgraphs?

A scalar like DateTime composed from two subgraphs is one type to the client, so differing formats produce values that cannot be compared or parsed consistently. Composition will not catch semantic divergence, only name and kind agreement. Publish a shared scalar specification and test serialization in each subgraph.

854. How do you handle file uploads in a federated architecture?

Terminate uploads at a dedicated subgraph or at a pre-signed object storage flow, then pass an identifier through the graph. Streaming multipart bodies through the router to arbitrary subgraphs is fragile and poorly supported. Returning a reference that other subgraphs can resolve keeps the graph clean.

855. How should error extensions be standardized across subgraphs?

Define a small taxonomy with stable codes, a machine-readable category, and a retryable flag, and apply it in every subgraph. Include the originating service so the router and clients can attribute failures. Free-form messages alone force clients to parse strings, which breaks as soon as wording changes.

{ "code": "RATE_LIMITED", "service": "pricing", "retryable": true }

856. How do you migrate a field from nullable to non-null without breaking clients?

First ensure the underlying data can never be null and measure violations in production, then tighten the type once telemetry is clean. Tightening is a breaking change by definition, so it belongs in a coordinated release. The reverse direction, non-null to nullable, is safe for clients but weakens guarantees.

857. What is the safe sequence for adding a required field to an entity?

Add it as nullable, populate it everywhere, and backfill historical data before making it non-null. Making it non-null immediately breaks any subgraph or resolver that returns the entity without that field. The staged approach also gives clients time to adopt the new field.

858. Is adding an enum value always a safe change?

It is safe for the schema but can break clients that switch exhaustively without a default branch. Clients should be documented as forward-compatible, and new values should be added deliberately with a client notification. Removing or renaming an enum value is genuinely breaking.

859. What role does usage reporting play in schema governance decisions?

It turns ownership debates into evidence: which clients query a field, how often, and with what latency. That data justifies deprecation, prioritizes optimization, and validates that a migration is complete. Without it, removals stall on fear and expensive fields stay invisible.

860. How would you detect a client that suddenly starts sending a much more expensive operation?

Track cost per operation hash per client and alert on outliers relative to that client's own baseline. Comparing against a global baseline hides changes in small clients. The alert should include the operation hash so the team can inspect the actual query.

861. How do you roll back a bad supergraph publication?

Republish the previous known-good supergraph artifact to the production variant so routers reload it. This must not require redeploying subgraphs, because the bad change may already be live in one of them. If the subgraph itself is faulty, roll back that subgraph too and let composition settle.

862. What are the practical limits on supergraph schema size, and how do you manage growth?

Large supergraphs increase router memory, validation time, and plan cache pressure, and every instance must hold the whole artifact. Growth is managed by pruning unused fields, avoiding duplicate type families, and splitting rarely used domains into contracts rather than the main graph. Monitor router startup time and memory as leading indicators.

863. What happens when two subgraphs declare the same custom directive with different arguments?

Composition generally requires directive definitions to agree, and mismatches surface as composition errors or as the directive being dropped. Ambiguity here is a governance signal that two teams invented overlapping metadata. Centralize shared directive definitions in a common schema package.

864. What distinguishes the API schema from the supergraph schema?

The supergraph schema includes join metadata and internal elements needed for planning, while the API schema is the client-facing subset with federation internals removed. Clients and codegen should always target the API schema. Confusing the two leads to generated code referencing fields no client can query.

865. How does the router decide that an incoming operation is valid?

It parses the document, validates it against the API schema's types, arguments, and directives, and enforces configured limits such as depth and cost. Only then does it build a plan. A validation failure returns a GraphQL error without touching any subgraph.

866. Should federation introspection fields such as _service and _entities be exposed publicly?

No, they are internal to composition and routing and reveal topology that clients should not depend on. The router's API schema excludes them, and subgraphs should be network-restricted so only the router can reach them. Public exposure also invites clients to bypass intended entry points.

867. How do you audit who changed a schema and why?

Route all publications through the registry with an associated commit, author, and change description. Pair that with the review record from the schema RFC or pull request. The audit trail matters most during an incident, when you need to correlate a schema change with a behavior shift.

868. Can one subgraph serve two different supergraphs?

Technically yes, but it couples the two graphs' composition and release cycles, so a change for one can break the other. If the graphs have different consumers or governance, prefer separate deployments even if they share code. Shared subgraphs are a hidden coupling that surfaces during incidents.

869. How do graph, variant, and contract relate to each other?

A graph is the overall federated API and its ownership boundary. A variant is an isolated copy of that graph's composition history, typically per environment. A contract is a filtered view of one variant's supergraph published as its own schema for a specific audience.

870. How can you estimate the impact of a schema change before rolling it out?

Replay recorded operations against the proposed supergraph in a staging router and compare responses and plan shapes. Schema checks catch static breaks, but replay catches semantic and performance regressions. Cost deltas from replay are the best predictor of a production latency change.

871. Which runbooks should exist for a federated platform team?

Runbooks for composition failure, a subgraph outage causing partial data, router rollback, registry unavailability, and an unexpected plan explosion. Each should name the dashboards to check and the exact rollback command. Without them, every incident becomes an improvisation under pressure.

872. How do you decide whether a new capability belongs in an existing subgraph or a new one?

Start from ownership: if the data and its lifecycle belong to an existing domain team, extending that subgraph is usually cheaper than adding a boundary. Create a new subgraph when the capability has its own data store, its own consumers, and a team that needs an independent deploy cadence. Adding a subgraph for organizational separation alone usually buys complexity instead of autonomy.

873. How does HotChocolate's code-first approach differ from a pure schema-first setup in .NET?

In code-first, C# types and resolver methods are the source of truth and HotChocolate derives the schema from them at startup or build time. Schema-first keeps an SDL document as the source of truth and binds it to C# via GraphQL.NET or HotChocolate's schema-first mode, which requires a second synchronisation step between the two artifacts.

public class Query
{
    public Book GetBook([Service] IBookRepository repo, int id) => repo.Find(id);
}

services.AddGraphQLServer().AddQueryType<Query>();

874. What do HotChocolate annotations such as [ID], [GraphQLName] and [UseProjection] actually influence?

Annotations are compile-time metadata that shape the generated schema and the middleware pipeline for a field. Type annotations change naming and nullability in the SDL, while middleware annotations register field-level behaviour such as projection, filtering or paging. Because they are resolved during schema building, adding one usually changes the published contract and should be reviewed like any schema edit.

875. When would you use HotChocolate source-generated resolvers instead of reflection-based resolvers?

Source-generated resolvers move resolver discovery and invocation from runtime reflection to compile-time generated code, which removes startup cost and makes trimming and ahead-of-time compilation viable. They are the right default for serverless or Native AOT deployments where the reflected pipeline either fails or blows the cold-start budget. The trade-off is less dynamic behaviour and a stricter build, so custom conventions relying on runtime inspection may need rework.

[QueryType]
public static partial class BookQueries
{
    [UseProjection]
    public static IQueryable<Book> GetBooks([Service] BookDbContext db)
        => db.Books;
}

876. How does [UseProjection] prevent over-fetching in a HotChocolate resolver that returns IQueryable?

UseProjection rewrites the LINQ expression tree so that only the fields selected in the GraphQL document are translated into the SQL projection. That keeps the database read aligned with the client's selection set instead of materialising whole entities. It only works when the resolver returns a composable IQueryable, so calling ToList before returning silently disables the optimisation.

877. What is the difference between HotChocolate filtering, sorting and paging middleware in terms of composition order?

Filtering and sorting must be applied before the query is materialised, while cursor pagination wraps the ordered result set and must run last in the pipeline. Wrong ordering produces either incorrect page boundaries or filters applied in memory. HotChocolate's attribute order or explicit middleware registration controls that sequence, so it is worth asserting the generated order in a test.

878. How does HotChocolate implement cursor pagination and what limits does it impose?

HotChocolate follows the Relay connection specification by wrapping results in edges, nodes, pageInfo and totalCount, deriving opaque cursors from the ordered key. It requires a stable, unique ordering key, otherwise cursors can skip or duplicate rows across pages. The middleware also raises an error when it cannot construct a valid cursor from the supplied sort combination.

879. What problem does a DataLoader solve in a GraphQL server and how is it registered in HotChocolate?

A DataLoader batches all keys requested during a single execution tick into one lookup and caches them for the remainder of the request, collapsing the N+1 pattern into a bounded number of calls. HotChocolate resolves DataLoaders from the request scope, so they must be registered as scoped services and keyed per data source. Reusing a DataLoader across requests would leak stale cache entries and must be avoided.

public class AuthorByIdDataLoader : BatchDataLoader<int, Author>
{
    protected override Task<IReadOnlyDictionary<int, Author>> LoadBatchAsync(
        IReadOnlyList<int> keys, CancellationToken ct) => _repo.GetManyAsync(keys, ct);
}

880. How do HotChocolate persisted operations change your security and deployment model?

Persisted operations replace the query text sent by clients with a hash or identifier that the server resolves against a known operation store. This eliminates arbitrary query execution, shrinks request payloads and makes it feasible to block unknown documents entirely in production. The operational cost is that the operation store becomes a deployment artifact that must be published and versioned in lockstep with the client.

881. What role does Fusion play in a HotChocolate distributed schema?

Fusion composes multiple subgraph schemas into a single supergraph that a gateway executes, including entity resolution across subgraphs. Each subgraph is independently deployable as long as its contract remains compatible, which is what allows team-level ownership. The gateway becomes the critical path, so schema composition must be validated in CI before any subgraph ships.

882. How do you express an entity reference in a HotChocolate Fusion subgraph?

You declare a key on the type and provide a reference resolver that can materialise the entity from that key alone. The gateway uses the reference resolver to route lookups to the owning subgraph during query planning. Forgetting the reference resolver yields a schema that composes but fails at execution time with an unresolved entity.

883. What is the main architectural difference between GraphQL.NET schema-first and HotChocolate code-first for a large .NET team?

GraphQL.NET treats the SDL as the contract and requires explicit C# graph types wired to resolvers, which suits teams that want schema review to happen in a language-neutral file. HotChocolate code-first inverts that, making C# the contract and the SDL a build output, which reduces duplication but couples schema review to code review. At scale the deciding factor is usually whether non-.NET stakeholders need to own schema changes.

884. How does field middleware work in GraphQL.NET and what should it not be used for?

Field middleware wraps a field resolver in a chain where each delegate can inspect the context, short-circuit or invoke the next resolver. It is appropriate for cross-cutting concerns such as authorisation, timing, caching and error normalisation. It should not carry business logic that differs per field, because the middleware chain is invisible from the resolver and becomes hard to reason about.

885. How do you add a custom validation rule to a GraphQL.NET schema?

Validation rules implement the document visitor contract and are registered on the schema's validation rule set, so every incoming document is checked before execution. Typical uses are depth limits, field complexity ceilings and banning introspection in production. Because rules run per document, they must be cheap and allocation-light.

886. What does an execution strategy control in GraphQL.NET?

The execution strategy governs how a parsed document is turned into resolver calls, including parallelism of sibling fields and serial execution for mutations. Swapping strategies lets you trade throughput against determinism or add instrumentation around the whole operation. Choosing a serial strategy for queries can hide N+1 issues by making them slower rather than fixing them.

887. How do you enforce query depth and complexity limits in a GraphQL.NET production deployment?

Document validation rules compute depth and complexity before execution and reject documents that exceed configured thresholds. The thresholds should be derived from real operation shapes rather than guesses, and complexity weights should reflect the actual cost of each field. Without these limits a single nested query can exhaust the thread pool.

888. How does Strawberry for Python generate its schema and what Python features does it lean on?

Strawberry derives the schema from decorated Python classes and functions using type hints, so the annotations are the contract. Dataclasses and Pydantic models both work as output types, and async resolvers are first-class. This makes the codebase readable but ties schema correctness to accurate typing discipline.

@strawberry.type
class Query:
    @strawberry.field
    async def book(self, id: strawberry.ID) -> Book:
        return await repo.get(id)

889. How do you implement per-request batching in Strawberry to avoid N+1 queries?

Strawberry exposes DataLoader built on asyncio, created per request and passed through the context so sibling resolvers share the same batch queue. Resolvers enqueue keys and await the loader, which flushes on the next event loop tick. Loaders must never be module-level singletons, or results from one request leak into another.

890. How does Strawberry support subscriptions and what transport does it expect?

Subscriptions are async generators exposed as fields and served over the graphql-transport-ws protocol through the chosen ASGI integration. Because each subscriber holds a long-lived connection, the ASGI worker count and event loop capacity become the real scaling constraint. Horizontal scaling requires a shared pub/sub backend so events reach whichever instance holds the socket.

891. What changed in Apollo Server 4 compared to version 3 that affects how you start a server?

Apollo Server 4 dropped the built-in Express integration and became a framework-agnostic core that you wire to a specific HTTP integration such as the standalone server or an Express middleware. Context and plugin signatures were also standardised, and the package was split so the integration is an explicit dependency. The result is more boilerplate but far less hidden behaviour.

const server = new ApolloServer({ typeDefs, resolvers });
await server.start();
const app = express();
app.use('/graphql', expressMiddleware(server, { context: async ({ req }) => ({ req }) }));

892. How does the Apollo Server plugin lifecycle let you implement request-level tracing?

Plugins receive hooks for server startup, request lifecycle and GraphQL execution phases, so you can open a span in requestDidStart and close it in the corresponding willSendResponse. Execution-level hooks add field granularity when needed. Because plugins are composed in order, an early-throwing plugin can suppress later instrumentation, so error paths need explicit coverage.

893. How would you enforce authentication and per-field authorisation in Apollo Server 4?

Authentication belongs in context construction, where the request is verified once and an identity is attached. Authorisation is then enforced in resolvers or, better, in a schema directive that wraps field resolution so the rules live next to the contract. Centralising the rule set prevents a new field from shipping without a check.

894. What is the difference between Apollo Server's schema landing page and a production-safe configuration?

Introspection and the embedded explorer are development conveniences that expose the full attack surface of the schema. In production you typically disable introspection and the landing page for untrusted clients while keeping a private schema registry as the source for tooling. Persisted operations let you keep introspection off without breaking client development.

895. How does GraphQL Yoga differ from Apollo Server in its plugin and middleware model?

Yoga is built on Envelop and WhatWG-compliant request/response objects, so plugins are Envelop plugins rather than Apollo-specific hooks. That makes the same plugin usable across any Envelop-based server and gives a lighter runtime footprint. The trade-off is a smaller first-party ecosystem and fewer managed cloud features.

896. What kinds of concerns are best implemented as Envelop plugins?

Envelop plugins wrap the execute and subscribe phases, making them ideal for persisted operations, response caching, error masking, logging and rate limiting. Because they operate on the document rather than on individual resolvers, they see the whole operation and can make decisions the resolver cannot. Stateful per-request data should go in the context, not in plugin-level closures.

897. How do you add response caching to GraphQL Yoga without caching user-specific data?

Use a caching plugin keyed on the normalised document plus the variables and a cache key derived from the authenticated identity or an explicit scope header. Anonymous operations can share entries; anything touching private data must include the identity in the key. TTLs should be short unless you also implement tag-based invalidation on mutation.

898. How does Mercurius integrate a GraphQL schema into a Fastify application?

Mercurius registers as a Fastify plugin, so the GraphQL endpoint participates in Fastify's lifecycle, hooks, logging and schema-based validation. Resolvers receive the Fastify request and reply, which means existing auth and rate-limit hooks apply to GraphQL automatically. It also supports defining the schema through Fastify's route-level configuration.

fastify.register(mercurius, {
  schema,
  resolvers,
  graphiql: false,
  jit: 1
});

899. What performance benefit does Mercurius's JIT compilation provide?

Mercurius can compile a document into a specialised resolver function the first time it is seen and reuse it for subsequent identical operations. That removes repeated parsing and traversal overhead for hot queries. The cache must be bounded, otherwise an attacker can grow it with unique documents.

900. How does gqlgen's code generation model work in Go?

You author the schema, then gqlgen generates interfaces, models and resolver stubs that you implement, keeping the schema as the contract while still getting static typing. Regeneration after a schema change surfaces every resolver that must be updated as a compile error rather than a runtime surprise. The generated code is committed and reviewed, which makes schema drift visible in diffs.

901. How do you control model generation and custom scalar mapping in gqlgen?

The gqlgen configuration file maps schema types to existing Go structs and binds scalars to marshaller implementations. Mapping to domain structs avoids duplicate model layers, at the cost of coupling the transport contract to internal types. Field-level binding lets you keep generated models for most types while overriding specific fields.

902. How does gqlgen handle the N+1 problem in practice?

gqlgen does not batch automatically, so batching is implemented either with dataloaden-generated loaders attached to the request context or by resolving whole object graphs in the parent resolver. Context-scoped loaders are the idiomatic answer because they batch by key across sibling fields. The common failure is constructing loaders outside the request context, which breaks batching entirely.

903. How does graphql-java's DataFetcher differ from a resolver in a JavaScript server?

A DataFetcher receives the environment with the source object, arguments and context, and returns either a value or a CompletableFuture for asynchronous resolution. The graphql-java engine then manages completion, so blocking calls inside a DataFetcher will stall the execution thread. Returning completed futures for cached values is a common optimisation.

904. What does Spring GraphQL add on top of graphql-java?

Spring GraphQL provides annotated controllers, argument resolution, exception handling, WebSocket and RSocket transport, and integration with the Spring security and observability stacks. It removes most boilerplate wiring while leaving the graphql-java engine in charge of execution. Schema-first remains the norm, with the SDL loaded from the classpath.

@QueryMapping
public Book book(@Argument String id) {
    return repository.findById(id).orElseThrow();
}

905. How do you instrument a Spring GraphQL server for tracing?

Spring GraphQL exposes instrumentation hooks that Micrometer consumes, producing spans per operation and per field with the configured observation registry. That gives field-level latency without hand-instrumenting resolvers. Field instrumentation is high-cardinality, so sampling and span limits must be tuned before production.

906. What does Hasura generate for you and where does custom logic live?

Hasura introspects PostgreSQL and generates a full CRUD schema including filters, aggregations, relationships and subscriptions, with permissions declared as metadata. Custom logic is added through actions that call HTTP handlers, or through event triggers and remote schemas. The trade-off is that complex domain rules either live in the database or in external services, not in the GraphQL layer.

907. How do Hasura permissions interact with a GraphQL query a client can craft?

Permissions are row-level and column-level rules evaluated per role, translated into SQL predicates rather than applied after fetching. That means an unauthorised row is never returned, which is stronger than resolver-level filtering. Session variables must come from a trusted source such as a verified JWT, otherwise a client can claim any role.

908. What is PostGraphile's core design decision and what does it imply for schema design?

PostGraphile derives the GraphQL schema from PostgreSQL schemas, tables, views, functions and comments, so the database is the single source of truth. Designing the API therefore means designing tables and views deliberately, using comments to shape descriptions and smart comments to control exposure. Complex read models are usually expressed as views or set-returning functions.

909. How do you extend a PostGraphile schema with logic the database cannot express?

You add schema plugins or makeExtendSchema calls that contribute additional types and resolvers, or wrap the generated schema with custom fields. Compute-heavy or third-party integrations are better placed behind those custom resolvers than inside the database. Plugins should stay narrow because they are executed in the same Node process and share its failure domain.

910. What are the main differences between Apollo Client 3 and Apollo Client 4 that affect cache behaviour?

Apollo Client 4 tightened type safety, moved to a modular reactive model and changed several defaults around cache interaction and local state. Reactive variables remain, but the recommend pattern increasingly pushes derived client state into typed cache policies. Migration work concentrates on cache configuration and on code that relied on loose typings in version 3.

911. How does Apollo Client normalise a response and when does that normalisation fail?

Objects are stored by a cache key derived from __typename plus id, with references from parent fields, so the same entity is shared across queries. Normalisation fails when an object lacks both id and _id, when different types reuse the same identifier, or when fields are embedded without a key. Those cases produce duplicate entries and stale UI after mutations.

912. When should you use a reactive variable instead of writing local state into the Apollo cache?

Reactive variables suit small, UI-scoped state such as filters or a selected tab that does not belong to the graph. Cache-resident local state is better when the value must be read by GraphQL field policies or persisted across queries. Mixing both for the same concern creates two sources of truth that drift.

913. How does urql's exchange model differ from a monolithic client?

urql builds every operation into a stream that flows through a chain of exchanges, each of which can transform, delay, deduplicate or short-circuit it. That makes cross-cutting concerns such as auth refresh, retries and caching composable and independently testable. The cost is that debugging requires understanding the stream composition rather than reading one large client implementation.

const client = createClient({
  url: '/graphql',
  exchanges: [dedupExchange, cacheExchange, fetchExchange]
});

914. What does urql's document cache exchange do and when does it produce stale results?

The document cache keys results by the query document and variables, replaying the stored result for identical operations. Because it does not normalise entities, a mutation touching one object will not update other queries that included it. Teams needing entity-level consistency move to the Graphcache exchange, which normalises by id and supports optimistic updates.

915. How would you implement automatic token refresh with a urql exchange?

An auth exchange inspects the operation result and, on an unauthorised error, pauses the stream, refreshes the token, then re-issues the operation with new headers. Because the exchange owns the retry, concurrent operations can share a single refresh promise instead of triggering a refresh storm. The refresh path must exclude the refresh call itself to avoid recursion.

916. How does the Relay compiler change the developer workflow compared to writing queries by hand?

The compiler scans source files for graphql literals, validates them against the schema, and emits generated artifacts plus a persisted query map. Builds fail on invalid or unused fields, which catches contract violations before runtime. It also enforces conventions such as unique operation names and fragment co-location.

917. What is fragment ownership in Relay and why does it matter at team scale?

A component declares the data it needs in its own fragment, and only that component reads those fields from its fragment reference. Parent components spread the fragment without knowing its contents, so a component's data requirements can change without touching every caller. This is what makes deletion of unused fields safe, because ownership makes usage discoverable.

const BookCard_book = graphql`
  fragment BookCard_book on Book { title coverUrl }
`;

918. How do Relay store updates work after a mutation that creates a new node?

Relay normalises records by id and applies declarative directives such as @appendNode, @prependNode or @deleteRecord to mutate the store from the mutation response. Connection handlers keep paginated lists consistent without manual refetching. If the server response omits the affected connection, the update silently does nothing, so the mutation payload must be designed for the client's update.

919. What is the difference between an optimistic update and a store update in Relay?

An optimistic update applies a temporary record immediately and is rolled back if the mutation fails, giving instant feedback. A store update runs on the real server response and is the authoritative change. Both are needed for a responsive UI that still converges on server truth.

920. How do you integrate GraphQL with TanStack Query without losing its caching semantics?

You wrap the GraphQL request in a query function and use a structured query key that includes the operation name and variables, letting TanStack Query own caching, deduplication and background refetching. The GraphQL client then acts only as a transport. Normalised entity updates must be handled by invalidating affected keys, since TanStack Query caches per key rather than per entity.

useQuery({
  queryKey: ['book', id],
  queryFn: () => client.request(BookDocument, { id })
});

921. When is TanStack Query a better fit than a normalising GraphQL cache?

It fits well when most screens are query-scoped, mutations are followed by targeted invalidation, and the app already uses TanStack Query for REST endpoints. It avoids the complexity of cache normalisation and optimistic entity merging. It fits poorly when many views render overlapping entity graphs that must update together instantly.

922. What belongs in a GraphQL codegen configuration file for a TypeScript monorepo?

The configuration declares the schema source, the documents glob, the output targets and the plugins that shape generated types and hooks. In a monorepo you typically generate a shared base types file once and per-package operation types next to their documents. Schema fetching should point at a checked-in or CI-published artifact rather than a running development server.

923. Why are generated hooks preferable to hand-written query wrappers in a typed codebase?

Generated hooks derive their variable and result types directly from the operation and schema, so a schema change becomes a compile error at every call site. Hand-written wrappers drift silently and duplicate the typing effort. The generated output should be excluded from review noise but regenerated deterministically in CI.

924. What are typed document nodes and what problem do they solve?

A typed document node pairs the parsed document with its generated result and variable types, letting the client infer types from the document itself rather than from a separate interface. That removes the manual generic parameters that previously drifted from the query. The pattern requires codegen to emit the typed node, so it is tied to your build pipeline.

925. How do you keep generated code from breaking builds when the schema evolves?

Pin the schema artifact used by codegen to a released version rather than the live endpoint, and regenerate in a dedicated CI job that fails on diff. That turns schema drift into a reviewable change instead of a surprise at runtime. Optionally allow a compatibility window where deprecated fields remain in the schema until all generated clients stop referencing them.

926. What are the trade-offs between schema-first and code-first at team scale?

Schema-first gives a language-neutral contract that product, client and backend teams can review together, at the cost of keeping SDL and resolver code in sync. Code-first removes duplication and gives compile-time guarantees, but schema review becomes a code review that non-backend stakeholders rarely attend. Large organisations often choose schema-first for shared contracts and code-first inside a single service boundary.

927. How do you share a GraphQL schema across packages in a monorepo?

Publish the schema as a versioned package or a generated artifact that all packages consume, with a single owner responsible for changes. A build graph then enforces that dependents regenerate and re-typecheck when the schema changes. Duplicating SDL files per package is the usual source of silent divergence.

928. What does contract-first design with mocks give a front-end team before the backend exists?

Once the schema and example operations are agreed, a mock server can serve realistic responses so the client can be built and tested against the contract. That shifts integration risk earlier and makes the contract itself the review artifact. Mocks must be generated from the same schema, otherwise they quietly diverge.

929. How do you keep mocks honest as the schema evolves?

Generate mock resolvers from the schema rather than hand-writing them, and run client tests against the contract in CI so a breaking schema change fails the build. Seeded, deterministic mock data makes snapshots stable. Hand-maintained fixtures should be limited to edge cases the generator cannot express.

930. What is a schema snapshot and how is it useful in review?

A snapshot is the fully rendered SDL committed to the repository, which makes every schema change appear as a textual diff. Reviewers can see added, removed and changed fields without running the server. It also gives tooling a stable input for diffing and client generation.

931. Which changes count as breaking in a GraphQL schema diff?

Removing a field or type, tightening nullability, removing an enum value, changing an argument type and making an optional argument required are all breaking for at least some clients. Adding a required argument or a non-null field on an output type can also break existing operations. Additive changes such as new optional arguments or new types are generally safe.

932. How would you wire breaking-change detection into a CI pipeline?

Publish the schema on every merge to the main branch, then in pull request CI diff the proposed schema against the published baseline and fail on breaking changes unless an approved label is present. The check should run before codegen so clients never build against an unreviewable contract. The baseline must be the deployed version, not the last commit, or drift accumulates.

933. Which schema lint rules are worth enforcing beyond breaking-change checks?

Naming conventions, mandatory descriptions on public types and fields, banning deprecated fields from new usage, and requiring list fields to be non-null with nullable elements are high-value rules. Linting also catches unused types that should be deleted. Rules should be enforced from day one, because retrofitting them across an existing schema is expensive.

934. How do you choose between a single GraphQL endpoint, a gateway and an edge deployment?

A single endpoint suits one team and one deployable, since it minimises moving parts. A gateway is warranted when multiple services or teams own parts of the graph and need independent releases. Edge deployment is justified by latency or data-residency requirements, not by fashion, because it multiplies the operational surface.

935. What does running GraphQL on serverless change about your architecture?

Each invocation is isolated, so in-memory caches, DataLoaders and connection pools do not survive between requests. Persisted operations, external caching and a pooled database proxy become necessary rather than optional. Cold starts also make schema construction cost visible, pushing teams toward pre-built or source-generated schemas.

936. How do you reduce GraphQL cold starts on a serverless runtime?

Move schema construction to build time, trim dependencies, avoid reflection-heavy resolver discovery and keep the bundle small. Provisioned concurrency or a minimum warm instance count covers the remaining tail latency. Measuring the p99 cold start separately from warm latency is essential, because averages hide the problem.

937. What are the practical constraints of running a GraphQL server at the edge?

Edge runtimes offer limited CPU time, restricted APIs and no persistent sockets, so long-running resolvers and database drivers may not work. Subscriptions are effectively impossible without a regional origin. The viable pattern is to run shallow, cacheable queries at the edge and forward complex or mutating operations to a regional gateway.

938. How do database connection limits constrain GraphQL scaling?

Every resolver chain can open a connection, so concurrency in the GraphQL layer multiplies pressure on a fixed database connection budget. Pooling per instance and bounding server concurrency keeps the total under the database limit. A proxy such as PgBouncer or a serverless driver is usually required once instance count scales horizontally.

939. Why do GraphQL subscriptions need sticky sessions or a shared pub/sub layer?

A WebSocket connection lives on one server instance, so events published elsewhere must be routed to that instance through a shared broker. Without stickiness during the handshake, load balancers can split connection and upgrade requests across instances. The broker becomes the scaling bottleneck and must be sized for fan-out.

940. How do you scale subscriptions horizontally without sticky sessions?

Terminate WebSockets at a dedicated gateway layer that maintains connections and subscribes to a shared topic per channel, decoupling connection count from GraphQL execution instances. Each event is then executed once and fanned out to subscribers. This also lets you enforce per-connection limits in one place.

941. What latency problems appear when a GraphQL gateway spans multiple regions?

Cross-region subgraph calls add round-trip latency per entity resolution step, and a single query can amplify it across many hops. Placing subgraphs with their data and routing clients to the nearest gateway region is the main mitigation. Caching and query planning that minimises hops matter more than raw compute in this topology.

942. How would you design a multi-region GraphQL deployment for data residency?

Run a full stack per region with region-local data stores and route users to their resident region, treating replication as an explicit, reviewed flow. The schema stays identical across regions while the data does not, which must be documented in the API contract. Global writes either stay regional or go through a coordinated service.

943. What should a GraphQL span contain in an OpenTelemetry trace?

An operation span carries the operation name, type and a hash of the document, with child spans per field, resolver and downstream call. Variables should be redacted or excluded, since they often contain personal data. The operation hash is what links traces to persisted operations and error groups.

span.setAttributes({
  'graphql.operation.name': 'BookDetail',
  'graphql.operation.type': 'query',
  'graphql.document.hash': docHash
});

944. Why is field-level tracing risky in production and how do you control it?

Field spans multiply cardinality and volume, so a deep query can generate thousands of spans per request. Sampling by operation, capping spans per trace and enabling field detail only for slow or errored operations keeps the cost bounded. Aggregated field latency metrics are usually more valuable than raw spans.

945. How do operation fingerprints improve error tracking for GraphQL?

Errors are grouped by operation name and normalised document hash rather than by raw text, so formatting differences and variable values do not fragment groups. That makes triage meaningful and links an alert to the exact persisted operation. Stack traces are still needed, but the fingerprint is the stable grouping key.

946. How should GraphQL errors be shaped so clients can act on them?

Use stable machine-readable extension codes plus a path so the client knows which field failed, and mask internal messages in production. Partial data with errors is often better than failing the whole operation. Error codes should be treated as part of the contract and versioned like fields.

947. How do feature flags help ship a risky schema change?

A flag can gate whether a new field resolves real data or falls back to a safe default, letting you deploy the schema before enabling behaviour. Flags decouple deployment from release, which is essential when the schema and the code cannot ship atomically. Each flag needs an owner and a removal date, or it becomes permanent debt.

948. How would you canary a new expensive field to a subset of traffic?

Enable the field for a small percentage of clients while watching resolver latency, error rate and downstream load against a control group. Because GraphQL clients select fields explicitly, you can also canary by client version rather than random traffic. Automatic rollback on latency regression is what makes the canary safe.

949. What does blue-green deployment mean for a GraphQL gateway?

Two identical gateway environments run side by side, with traffic switched after the new one passes health and smoke checks, allowing instant rollback. Because the gateway holds no state beyond connections, this works cleanly for query and mutation traffic. Subscriptions need draining and reconnection handling, which is where most blue-green incidents occur.

950. How do you drain WebSocket connections during a gateway deployment?

Stop accepting new connections on the old environment, signal clients to reconnect with a retry hint, and wait for a grace period before terminating. Clients should implement jittered exponential backoff so the reconnect wave does not overwhelm the new environment. The grace period must exceed the longest expected subscription message interval.

951. How would you structure a test pyramid for a GraphQL service?

Unit tests cover schema building, pure resolver logic, custom scalars and authorisation rules without any transport. Integration tests execute real documents against an in-memory server with a real or containerised database, which is where most contract behaviour should be asserted. End-to-end tests are reserved for a small set of critical journeys through the real gateway and clients.

952. How do you unit test a resolver that depends on a DataLoader?

Construct the resolver with a fake loader that returns deterministic values and assert both the returned data and the keys it requested. That verifies the resolver's contract with the batching layer without touching a database. A separate test should confirm that the real loader actually batches sibling calls in one execution.

953. What makes an integration test for GraphQL different from testing a REST handler?

The unit under test is a document, so the test asserts on a selection set, partial errors and the shape of the data rather than on a single endpoint response. You also need to cover variable coercion, null propagation and field-level failures, which have no REST equivalent. Executing the document against the real schema is what catches resolver wiring mistakes.

const res = await server.executeOperation({ query, variables });
expect(res.body.singleResult.errors).toBeUndefined();

954. How do contract tests protect a GraphQL client from server changes?

Contract tests execute every operation the client can send against the server schema and assert that the response validates and contains the expected fields. They catch removals and type changes before deployment, and they document the exact surface the client depends on. They must run against the candidate server version, not only against mocks.

955. What should end-to-end GraphQL tests cover that lower layers cannot?

They exercise the real network path, gateway composition, authentication middleware, CDN caching and client cache behaviour together. That exposes problems such as header stripping, persisted operation mismatches and WebSocket routing that unit tests never see. Because they are slow and flaky, they should be few and focused on revenue-critical flows.

956. How do you build deterministic fixtures for GraphQL integration tests?

Seed a known dataset with fixed identifiers and reset the database to a snapshot before each suite, so assertions do not depend on ordering or prior state. Factories should generate values from a seeded random source when variety is needed. Tests that rely on auto-increment ids or current timestamps become order-dependent and flaky.

957. How do you test subscriptions reliably in CI?

Start the server on an ephemeral port, open a real transport connection, publish an event and await the message with a bounded timeout. Asserting that no message arrives within a window is also a valid test, but needs a generous margin to avoid flakiness. Using the real pub/sub client rather than a fake is what verifies the wiring.

958. Why are generic load tests misleading for a GraphQL endpoint?

A GraphQL endpoint's cost depends entirely on the documents sent, so hammering a trivial query measures almost nothing. Realistic tests replay a distribution of actual production operations with representative variables. The mixture, not the request rate, determines database load and tail latency.

959. How do you obtain realistic operations for performance testing?

Sample persisted operations and variable shapes from production telemetry, keeping the top operations plus a long tail, and redact personal data. Replaying that distribution in a staging environment reproduces real database access patterns. Synthetic queries tend to be either too simple or pathologically nested, both of which mislead capacity planning.

960. How would you design a load test scenario for a paginated feed query?

Model a virtual user that walks several pages with increasing cursors, includes the expensive fields, and mixes in mutations that invalidate cached data. Asserting only on throughput hides the cost of deep pagination, which degrades as offsets grow. Latency percentiles per operation type are the meaningful signal.

961. How do you translate a GraphQL cost model into capacity numbers?

Measure the database and CPU cost per unit of query complexity for each operation family, then multiply by the expected operation mix and peak concurrency. That yields resource requirements per instance, which you validate with a load test rather than trusting the model. Recompute when the top operations change, because cost per operation is not stable.

962. How can you attribute GraphQL cost to individual teams in a federated graph?

Tag metrics and spans with the subgraph or owning domain so resolver time and downstream calls roll up per team. Combined with client identifiers, that produces chargeback data for both producers and consumers. Attribution only works if field ownership is recorded in the schema registry, not inferred from code layout.

963. How do you rate-limit GraphQL fairly when one operation can cost a thousand times another?

Apply a cost estimate per operation, derived from field weights and list multipliers, and deduct it from a per-client budget rather than counting requests. That prevents a single deep query from consuming the same allowance as a cheap lookup. The estimator must be conservative, and rejected operations should receive a clear cost-related error code.

964. What is a batching or aliasing attack against a GraphQL endpoint?

An attacker repeats the same expensive field with many aliases in one document, or sends many operations in a single request, to bypass per-request limits. Depth limits do not help because the query stays shallow. Mitigations include per-document field counts, alias budgets, disabled batching and cost-based rejection.

965. What does an incremental migration from REST to GraphQL look like?

Start by exposing existing services as GraphQL fields backed by the current REST or domain layer, so clients can adopt the new endpoint without backend rewrites. Migrate one client surface at a time, keeping REST alive for everything not yet covered. Each migrated surface gives you usage data to justify the next step.

966. How does the strangler pattern apply to a GraphQL gateway replacing a REST API?

The gateway fronts both new resolvers and legacy REST calls, and traffic is shifted route by route until the legacy path has no consumers. Because the gateway hides the backend, you can replace implementations without touching clients. The pattern fails when teams keep adding features to the legacy path and the migration never converges.

967. How do you keep REST and GraphQL consistent while both are live?

Both surfaces should call the same domain services rather than duplicating business rules, so behaviour stays identical by construction. Shared validation and authorisation logic belongs below the transport layer. Divergence usually appears where each surface queries the database directly.

968. In what order should clients be migrated to GraphQL?

Start with the client that benefits most from reduced round trips and has the strongest release cadence, typically a web app owned by an enthusiastic team. Mobile clients follow once the contract is stable, because their release cycle makes rollback expensive. Public or partner integrations migrate last, when the schema has matured.

969. How do you know a REST endpoint is safe to deprecate?

Instrument the endpoint to record consumer identity and call volume, then require zero traffic from known consumers over a full business cycle before removal. Announce a deprecation window, add warning headers and offer a GraphQL equivalent documented for each consumer. Silent removal based only on log sampling is how outages happen.

970. What is the least risky way to adopt GraphQL inside an existing monolith?

Add a GraphQL layer as a thin facade over the existing service layer in the same deployable, exposing a handful of read operations first. This avoids distributed-systems overhead while proving the developer experience. Splitting into subgraphs should wait until ownership boundaries are clear.

971. How should schema ownership be organised across teams?

Each field or type has exactly one owning team, recorded in the schema registry, and only that team can change it. Consumers request changes through an additive contract process rather than editing another team's types. Shared types without a clear owner become the main source of uncoordinated breakage.

972. What does a schema review process look like in a mature organisation?

Proposed changes are reviewed as a diff with descriptions, deprecation plans and expected client impact, then approved by the owning team and any affected consumers. Automated checks cover naming, breaking changes and usage of deprecated fields. The review exists to make cost and compatibility explicit, not to gatekeep naming preferences.

973. How do you evaluate a GraphQL documentation portal?

It should render descriptions from the schema, support searching by field or type, show deprecations, and offer runnable examples against a safe environment. The decisive factor is whether it stays in sync automatically, because a stale portal is worse than none. Access control matters when parts of the schema are internal.

974. How do you keep a local schema in sync for IDE tooling?

Generate the SDL into the repository from the build or a registry pull, and point IDE plugins at that local file rather than the live endpoint. That gives completion and validation without production credentials. A pre-commit or watch task keeps the file current as developers edit.

975. What belongs on a GraphQL pull request review checklist?

Confirm the change is additive or has a deprecation path, descriptions are present, nullability is intentional, authorisation is enforced, and resolvers avoid N+1 patterns. Check that client operations are updated and that complexity impact is understood for new list fields. The checklist should be short enough that reviewers actually use it.

976. What are the most common failure modes when teams adopt GraphQL?

Treating GraphQL as a database proxy that exposes tables directly, ignoring N+1 until latency collapses, and letting the schema grow without ownership or deprecation discipline. Another frequent failure is building a gateway before there are multiple services, adding operational cost for no benefit. Most adoption problems are organisational rather than technical.

977. When is GraphQL the wrong choice for an API?

It is a poor fit for simple CRUD services with one consumer, for high-volume binary or file transfer, and for teams without the discipline to manage a shared contract. If clients need a single fixed payload and strong HTTP caching, REST is simpler and cheaper. Choosing GraphQL should follow a concrete client-aggregation problem, not a preference.

978. How would you summarise the cost-benefit case for GraphQL to leadership?

Frame benefits as fewer round trips, faster client feature delivery and a single reviewed contract, quantified against current client release friction. Costs are the gateway and observability infrastructure, schema governance effort and a steeper learning curve. The honest conclusion is often that GraphQL pays off with many clients and multiple teams, and does not for a small single-client product.

979. How do persisted operations change your rollout plan for clients?

The operation store must contain every document a client can send before that client is released, so publishing operations becomes part of the deployment pipeline. Roll out in allow-list mode that logs unknown documents before enforcing rejection. Enforcement without a monitoring period breaks clients you did not know existed.

980. How does automatic persisted query negotiation differ from a strict allow-list?

Automatic persisted queries let a client send only a hash first, and fall back to sending the full document if the server does not recognise it, which is convenient but permits arbitrary queries. A strict allow-list rejects anything unknown, trading flexibility for a hard security boundary. Production public APIs usually want the strict mode, with a monitored transition.

981. How can you make GraphQL queries cacheable by a CDN?

Send queries as GET requests with encoded variables and a stable operation name, then set cache headers derived from the fields touched and the caller's identity. Only operations that are identical for every viewer can be shared, so authenticated responses need private caching or key-based partitioning. Mutations always bypass the shared cache.

982. What is the argument for non-null list elements but a nullable list in schema design?

Making the list itself nullable lets you represent "no data" distinctly, while non-null elements promise that a null item never appears and avoids clients handling nulls inside collections. The combination expresses intent precisely and reduces defensive client code. Overusing non-null everywhere is worse, because one failing field then nulls out an entire parent object.

983. How does null propagation affect error handling in a deep query?

A null returned from a non-null field propagates upward until it reaches a nullable parent, potentially nulling a large part of the response. That is why non-null assertions on resolvers are a design decision, not a default. Errors should be raised explicitly with paths so clients understand exactly which field failed.

984. What discipline should custom scalars follow?

Each custom scalar needs a documented format, strict parse and serialise functions, and validation that rejects malformed input at the boundary. Scalars should be used for genuine semantic types such as DateTime or Money, not to avoid defining an object. A permissive scalar silently pushes validation into the client.

985. Why are directives a better place for authorisation than individual resolvers?

A directive applied in the schema makes the rule visible in the contract and impossible to forget on a new field, and it can be enforced uniformly by middleware. Resolver-level checks are easy to omit and hard to audit across hundreds of fields. Directives also let reviewers see the intended access level in a schema diff.

directive @auth(requires: Role!) on FIELD_DEFINITION

986. How do you deprecate a field without breaking clients?

Mark it deprecated with a reason and a replacement, keep resolving it, and track usage per client so you know when it is truly unused. Removal happens only after usage reaches zero across a full release cycle for every consumer. Deleting a deprecated field because it looks old is a common cause of production incidents.

987. How do field usage analytics change schema evolution?

Recording which fields each client actually selects lets you quantify blast radius before a change and identify dead schema surface for removal. It also reveals expensive fields that few clients need. Analytics should be sampled and aggregated, since storing full documents per request is costly and privacy-sensitive.

988. Should a public GraphQL API be versioned or continuously evolved?

The GraphQL convention is continuous evolution with additive changes and deprecation rather than URL versioning, which keeps a single schema for all clients. Versioning is still reasonable for breaking business changes that cannot be expressed additively. Either way, the deprecation policy and communication cadence matter more than the mechanism.

989. How would you cache at the gateway without breaking per-user data?

Cache whole operations only when the document is viewer-independent, and key everything else on a hash of the identity claims plus variables. Entity-level caching in the gateway is riskier because invalidation across subgraphs is hard to get right. Short TTLs plus tag-based purge on mutation are the pragmatic middle ground.

990. What GraphQL design choices matter most for mobile clients?

Keeping payloads small through precise selection sets, avoiding deeply nested defaults and designing mutations that return enough data for a local cache update. Connection-based pagination works well with mobile list virtualisation. Mobile also benefits from persisted operations, which cut request size on constrained networks.

991. How do you support offline-capable clients with GraphQL?

The client persists a normalised cache and queues mutations locally, replaying them when connectivity returns with idempotency keys to avoid duplicates. Conflict resolution must be defined per mutation, because the server cannot order offline writes. Subscriptions need resubscription and catch-up logic after reconnection.

992. How does file upload work in GraphQL and why is it often discouraged?

The multipart request specification lets a mutation accept an upload scalar alongside variables, but it complicates gateways, caching and CDN behaviour. Most architectures instead issue a signed upload URL from a mutation and pass the resulting identifier into the domain mutation. That keeps large binary payloads out of the GraphQL execution path.

993. When would you choose live queries over subscriptions?

Live queries re-execute a client's query when dependent data changes, which suits dashboards where the selection set varies. Subscriptions require the server to define explicit event streams and push payloads, which scales better when events are well-modelled. Live queries are convenient but can multiply execution cost under load.

994. How do you resolve a conflict when two subgraphs claim the same entity field?

Ownership must be explicit in the schema registry so composition fails when two teams define the same field. One subgraph owns the canonical field and others contribute extensions or computed fields. Leaving duplicate definitions to resolver precedence produces silent, environment-dependent behaviour.

995. How do you observe query planning in a federated gateway?

Emit the query plan, its fetch count and per-subgraph timing as trace attributes and metrics, then alert on plans that fan out unexpectedly. A plan shape change after a subgraph deploy often explains a latency regression better than any single resolver metric. Sampling plans by operation hash keeps the data comparable over time.

996. What signals tell you a GraphQL adoption is succeeding or stalling?

Success shows as rising client adoption, shrinking time to add a field, and fewer client-side aggregation endpoints. Stalling shows as a schema that only one team edits, growing REST wrappers around the graph, and repeated incidents from unowned fields. Measuring delivery lead time for a cross-team field is the clearest single indicator.

997. What role does a schema registry play in a multi-team GraphQL platform?

It stores every published schema version with ownership metadata, runs composition and lint checks on each proposal, and serves as the trusted baseline for diffs and client codegen. That makes compatibility a platform guarantee rather than a per-team convention. Without a registry, breaking-change detection degrades into comparing against whatever schema a developer happens to have locally.

Recipes 14 questions

Copy-usable recipes: connection pagination, authorized resolver, DataLoader, mutation with result union, DateTime scalar, auth directive, complexity rule, persisted queries, subscriptions, federation entity, codegen, error formatter, integration test, HotChocolate registration.

998. How do I define a minimal schema with a connection type and cursor pagination?

Model the list as an edge/node connection with opaque cursors encoded as base64 offsets, and expose forward pagination arguments (first, after). Resolving pageInfo correctly lets clients detect more pages without total counts.

type PageInfo { hasNextPage: Boolean!, endCursor: String }

type UserEdge { cursor: String!, node: User! }

type UserConnection { edges: [UserEdge!]!, pageInfo: PageInfo! }

type Query {
  users(first: Int = 20, after: String): UserConnection!
}

# resolver
const decode = (c) => c ? Number(Buffer.from(c, 'base64').toString()) : 0;
const encode = (i) => Buffer.from(String(i)).toString('base64');
async users(_, { first, after }) {
  const start = decode(after);
  const rows = await db.user.findMany({ skip: start, take: first + 1 });
  const hasNextPage = rows.length > first;
  const edges = rows.slice(0, first).map((node, i) =>
    ({ cursor: encode(start + i + 1), node }));
  return { edges, pageInfo: { hasNextPage, endCursor: edges.at(-1)?.cursor ?? null } };
}

999. How do I enforce an authorization check inside a resolver using the GraphQL context?

Pass the authenticated principal into the context when the server is created, then guard the resolver with a small helper that throws a typed GraphQL error. Never read raw tokens inside resolvers; keep identity resolution at the transport layer.

// context factory
const context = async ({ req }) => ({ user: await verify(req.headers.authorization) });

const requireRole = (ctx, role) => {
  if (!ctx.user) throw new GraphQLError('UNAUTHENTICATED', { extensions: { code: 'UNAUTHENTICATED' } });
  if (role && !ctx.user.roles.includes(role))
    throw new GraphQLError('FORBIDDEN', { extensions: { code: 'FORBIDDEN' } });
  return ctx.user;
};

type Mutation {
  deletePost(id: ID!): Boolean!
}

async deletePost(_, { id }, ctx) {
  const user = requireRole(ctx, 'admin');
  await db.post.delete({ where: { id, ownerId: user.id } });
  return true;
}

1000. How do I batch foreign key lookups with DataLoader to avoid the N+1 problem?

Create one DataLoader per request on the context, keyed by the foreign key, so all sibling resolvers are coalesced into a single query in the same tick. The batch function must return results in the exact same order and length as the keys.

import DataLoader from 'dataloader';

const authorLoader = () => new DataLoader(async (ids) => {
  const rows = await db.author.findMany({ where: { id: { in: [...new Set(ids)] } } });
  const byId = new Map(rows.map((r) => [r.id, r]));
  return ids.map((id) => byId.get(id) ?? null);
});

// context
const context = async ({ req }) => ({ user: req.user, loaders: { author: authorLoader() } });

// field resolver
Post: { author: (post, _a, ctx) => ctx.loaders.author.load(post.authorId) }

1001. How do I write a mutation with an input type that returns a result union of success and user error?

Declare one input type for the payload and a union of possible outcomes, then resolve the union's __typename from the returned object. Clients must select fields per concrete type, which makes error handling exhaustive and type-safe.

input CreateUserInput { email: String!, name: String! }
type User { id: ID!, email: String!, name: String! }
type ValidationError { field: String!, message: String! }
union CreateUserResult = User | ValidationError

type Mutation { createUser(input: CreateUserInput!): CreateUserResult! }

async createUser(_, { input }, ctx) {
  if (!input.email.includes('@'))
    return { __typename: 'ValidationError', field: 'email', message: 'Invalid email' };
  const user = await db.user.create({ data: input });
  return { __typename: 'User', ...user };
}

// resolver map
CreateUserResult: { __resolveType: (obj) => obj.__typename }

1002. How do I implement a custom DateTime scalar with serialize and parseValue?

Use GraphQLScalarType and normalise to one canonical representation, typically an ISO string in UTC, for every direction. Validate on the way in so bad client input fails at the coercion boundary rather than deep in business logic.

import { GraphQLScalarType, Kind } from 'graphql';

export const DateTime = new GraphQLScalarType({
  name: 'DateTime',
  description: 'ISO-8601 UTC timestamp',
  serialize(value) {
    const d = value instanceof Date ? value : new Date(value);
    if (Number.isNaN(d.getTime())) throw new TypeError('DateTime cannot represent value');
    return d.toISOString();
  },
  parseValue(value) {
    if (typeof value !== 'string') throw new TypeError('DateTime must be a string');
    const d = new Date(value);
    if (Number.isNaN(d.getTime())) throw new TypeError('Invalid DateTime');
    return d;
  },
  parseLiteral(ast) {
    if (ast.kind !== Kind.STRING) throw new TypeError('DateTime must be a string literal');
    return new Date(ast.value);
  },
});

1003. How do I create a custom directive that enforces a required role on a field?

Declare the directive in SDL and implement a mapper that wraps the field's resolver with the authorization check. The wrapper runs before the original resolver, so the rule applies uniformly wherever the directive is used.

directive @auth(requires: Role!) on FIELD_DEFINITION | OBJECT
enum Role { USER ADMIN }

type Query { revenue: Int! @auth(requires: ADMIN) }

const authDirective = (next, source, { requires }, ctx) => {
  const user = ctx.user;
  if (!user || !user.roles.includes(requires))
    throw new GraphQLError('Not authorized', { extensions: { code: 'FORBIDDEN' } });
  return next(source, {}, ctx, info);
};

// makeExecutableSchema
schemaTransforms: [],
directiveResolvers: { auth: authDirective },
directiveTypeDefs: `directive @auth(requires: Role!) on FIELD_DEFINITION | OBJECT`,
resolvers: { Role: { USER: 'USER', ADMIN: 'ADMIN' } }

1004. How do I add a query depth and complexity limit rule to protect the server?

Use validation rules that compute depth and cost at parse time and reject operations before execution. Assign a cost per field and multiply by list arguments such as first so wide pagination requests cannot bypass the budget.

import depthLimit from 'graphql-depth-limit';
import { createComplexityLimitRule } from 'graphql-validation-complexity';

const complexityRule = createComplexityLimitRule(1000, {
  onCost: (cost) => { if (process.env.NODE_ENV !== 'production') console.log('cost', cost); },
  scalarCost: 1,
  objectCost: 2,
  listFactor: 10,
});

const server = new ApolloServer({
  schema,
  validationRules: [depthLimit(8), complexityRule],
});

1005. How do I configure persisted queries so only trusted documents are executed?

Extract every operation at build time into a manifest keyed by hash, then serve only those hashes at runtime and disable arbitrary query strings. Requests carrying an unknown hash are rejected, which blocks ad-hoc queries and protects against resource abuse.

// build step: emit map of sha256 -> query text
// apollo-server with persisted queries
import { createHash } from 'node:crypto';
import manifest from './persisted-manifest.json' assert { type: 'json' };

const persisted = {
  get(hash) { return manifest[hash]; },
  set() { throw new Error('PersistedQueryNotSupported'); },
};

new ApolloServer({
  schema,
  persistedQueries: { cache: persisted },
  // production: reject full query strings
  allowBatching: false,
});

1006. How do I set up a subscription server with graphql-ws?

Mount useServer on the same HTTP server and give it the schema plus a context builder that re-authenticates the socket. Publish events through a pubsub instance shared with the mutation resolvers.

import { WebSocketServer } from 'ws';
import { useServer } from 'graphql-ws/use/ws';
import { PubSub } from 'graphql-subscriptions';

const pubsub = new PubSub();
const wsServer = new WebSocketServer({ server: httpServer, path: '/graphql' });

useServer({
  schema,
  context: (ctx) => ({ user: verify(ctx.connectionParams?.token) }),
  onConnect: async (ctx) => { if (!ctx.connectionParams?.token) return false; },
}, wsServer);

// resolver
Subscription: { postAdded: { subscribe: () => pubsub.asyncIterator('POST_ADDED') } }

// publish
await pubsub.publish('POST_ADDED', { postAdded: post });

1007. How do I expose a federation entity with @key and a reference resolver?

Mark the type as an entity with @key and implement __resolveReference so the gateway can fetch it from a representation object. The subgraph returns only the fields it owns; other subgraphs extend the same entity.

# users subgraph
type User @key(fields: "id") {
  id: ID!
  email: String!
}

type Query { user(id: ID!): User }

# resolver
User: {
  __resolveReference: (ref, ctx) => ctx.loaders.user.load(ref.id),
}

# reviews subgraph extends it
extend type User @key(fields: "id") {
  id: ID! @external
  reviews: [Review!]!
}

1008. How do I configure graphql-codegen to produce typed operations for a TypeScript client?

Run the CLI with a client preset so each .graphql document emits a typed document node plus its result and variable types. Point schema at the local SDL or a live endpoint and commit the generated output for reproducible builds.

# codegen.ts
import type { CodegenConfig } from '@graphql-codegen/cli';

const config: CodegenConfig = {
  schema: 'src/schema.graphql',
  documents: ['src/**/*.graphql'],
  ignoreNoDocuments: true,
  generates: {
    'src/gql/': {
      preset: 'client',
      config: { useTypeImports: true, dedupeFragments: true },
    },
  },
};
export default config;

// package.json: "codegen": "graphql-codegen --config codegen.ts"

1009. How do I write an error formatter that hides internal server messages?

Inspect the error's extensions.code and replace any message that is not explicitly marked as safe. Log the original error server-side with a correlation id so operators can still diagnose it.

const SAFE = new Set(['GRAPHQL_VALIDATION_FAILED', 'BAD_USER_INPUT', 'UNAUTHENTICATED']);

new ApolloServer({
  schema,
  formatError: (formatted, error) => {
    const code = error.extensions?.code ?? 'INTERNAL_SERVER_ERROR';
    if (SAFE.has(code)) return formatted;
    console.error('masked error', error);
    return { message: 'Something went wrong', extensions: { code: 'INTERNAL_SERVER_ERROR' } };
  },
});

1010. How do I write an integration test that executes an operation against the schema?

Run graphql() against the real schema with a test context so validation, resolvers and serialization are all exercised. Assert on both data and errors, and use a fresh context per test to avoid DataLoader cache leakage.

import { graphql } from 'graphql';
import { makeExecutableSchema } from '@graphql-tools/schema';

const schema = makeExecutableSchema({ typeDefs, resolvers });

test('returns the current user', async () => {
  const result = await graphql({
    schema,
    source: `query { me { id email } }`,
    contextValue: { user: { id: '1', roles: ['USER'] }, loaders: makeLoaders() },
  });
  expect(result.errors).toBeUndefined();
  expect(result.data.me).toEqual({ id: '1', email: 'a@b.co' });
});

1011. How do I register a HotChocolate server with projection, filtering, sorting and pagination?

Add the four middleware components in AddGraphQLServer; they translate arguments into EF Core queries so paging and filtering happen in the database. Keep UseProjection last so only selected columns are queried.

builder.Services
  .AddGraphQLServer()
  .AddQueryType<Query>()
  .AddProjections()
  .AddFiltering()
  .AddSorting()
  .AddDbContextCursorPagingProvider();

public class Query
{
    [UseProjection]
    [UseFiltering]
    [UseSorting]
    public IQueryable<Book> GetBooks([Service] AppDbContext db) => db.Books;
}

// client
// query { books(first: 10, where: { title: { contains: "graph" } }, order: { title: ASC }) { nodes { title } } }