Writing queries, mutations, and subscriptions with typed document nodes
Operations are how you interact with the GraphQL API. We use typed document nodes instead of auto-generated hooks, providing better flexibility and type safety.Now that you understand the architecture and key concepts, let's learn how to write GraphQL operations. This section covers queries, mutations, and subscriptions—the core ways you interact with the GraphQL API.
Queries
Basic Query Structure
Using in Components
Before and After: Hooks vs Document Nodes
❌ Don't do this - Auto-generated Hooks
Old Pattern: Using generated hooks
Problems:
Less flexible than standard Apollo hooks
Harder to mock in tests
Couples you to specific codegen output
Often uses magic strings
✅ Do this - Typed Document Nodes
New Pattern: Using document nodes with useQuery
Benefits:
All Apollo Client options available
Easier to test: renderWithApollo runs the real hooks against schema-driven mock handlers (see Testing GraphQL Components)
Better IDE support and debugging
Type-safe variables with enums
Working with Enums
GraphQL enums are generated as native TypeScript enums, providing type safety for all enum values.
Using Generated Enums
❌ Don't do this - Magic Strings
✅ Do this - Type-Safe Enums
Native enums
Codegen emits each GraphQL enum as a native TypeScript string enum:
This provides:
Nominal typing: assigning "ACTIVE" to a ChatroomStatus variable, argument, or field fails tsc
A runtime object, which a bundler drops when a module never reads it
Autocomplete in IDEs
Compile-time validation
TypeScript still allows status === "ACTIVE" and case "ACTIVE":, because a string equal to a member's value is comparable to the enum, so pnpm lint:graphql rejects them. Compare with ChatroomStatus.Active.
Mutations
Basic Mutation
Using Mutations
A mutation can fail, and the user needs to see that it did: the handler shows a failure state. It does not call logger.errorAndReport: the shell's Apollo client reports each Apollo error once, and only when the backend cannot have (see Error Handling).
Field Selection Best Practices
❌ Don't do this - Over-fetching
✅ Do this - Request Only What You Need
❌ Don't do this - Query Type Extractions
Problem: Extracting types from query results using TypeScript indexed access
Partial improvement: You can use NonNullable to handle nullability (NonNullable<Query["field"]>["subfield"]), but this still has the same limitations. See the Fragments guide for the migration path to a component-owned fragment.
Variable Usage
❌ Don't do this - Unused Variables
pnpm lint:graphql fails with @graphql-eslint/no-unused-variables
✅ Do this - Use All Declared Variables
Subscriptions
Basic Subscription
Using Subscriptions
Error Handling
Who reports an error
Components never report an Apollo error. Each shell's Apollo client (apps/responder and apps/console, in src/integrations/mundi-apollo.ts) passes reportApolloError from @prepared911/util-apollo as error.onError. It runs once per failed operation, after RetryLink has given up, and:
Reports a failure only the browser can see: a network or transport failure (ServerError, ServerParseError, a subscription socket that closed for good) or an exception inside the link chain. The report carries the operation name, the operation type, the error class, and a ServerError's HTTP status. It never carries the variables, which can hold caller and call data.
Skips an error the server returned in its response (CombinedGraphQLErrors, and CombinedProtocolErrors from the router): the backend has already reported it.
Skips an expired session (UNAUTHENTICATED or UNAUTHORIZED, HTTP 401, or a subscription socket closed with 4401 or 4403): the client's auth.onUnauthorized deals with it.
Reports one failure once when it fans out: a failed BatchHttpLink request fails every operation in the batch, and a socket that closes for good fails every active subscription. Reports with the same error class, status, and message within two seconds count as one. When that window ends with repeats suppressed, one more report, Apollo operation failure repeated, says how many (suppressedCount) and which operations they were (operationNames, at most ten).
Skips a request cancelled through an AbortSignal.
So a component only shows the user a failure state. Call logger.errorAndReport (from @prepared911/telemetry) only for an unexpected exception in your own code. In the responder, console, and wallboard shells a report becomes a Datadog log and a RUM error; elsewhere it only prints.
Query Error Handling
Mutation Error Handling
In Apollo 4 the promise from mutate rejects on any error, even when you pass onError, so catch it where you call it and show the failure there (see Using Mutations). To tell a permission error from other failures, check the caught error the same way:
Operation Naming Conventions
All operations must be named for better debugging and tooling:
Naming conventions:
Queries: Start with Get or describe the data (e.g., GetUser, SearchIncidents)
Mutations: Start with a verb (e.g., UpdateChatroom, CreateIncident)
Subscriptions: Start with On (e.g., OnChatroomUpdate, OnMessageReceived)
The lint gate enforces PascalCase names, camelCase variables, the On prefix for subscriptions, and no Query/Mutation/Subscription/Fragment suffix: codegen appends those, so an operation named ThingsQuery would generate ThingsQueryQuery. The Get prefix and the mutation verb are conventions that nothing enforces: neither lint nor a review agent checks them, so follow them in code review.
Optimistic Updates
For better UX, use optimistic updates with mutations:
Best Practices Summary
✅ Do
Use typed document nodes with standard Apollo hooks
Import and use generated enum types
Name all operations descriptively
Request only the fields you need
Handle errors appropriately
Give each component its own fragment for the fields it reads, and spread it into the route's query
❌ Don't
Use auto-generated hooks from old patterns
Hard-code enum values as strings
Over-fetch data you don't use
Define variables you don't use
Use anonymous operations
Ignore TypeScript errors on variables
Once you're comfortable writing operations, the next section covers where to organize them using the colocation pattern.