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.
Old Pattern: Using generated hooks
Problems:
New Pattern: Using document nodes with useQuery
Benefits:
GraphQL enums are generated as TypeScript const enums, providing type safety for all enum values.
Our codegen configuration uses enumsAsConst: true, which generates:
This provides:
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).
Problem: Extracting types from query results using TypeScript indexed access
Issues:
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.
pnpm lint:graphql fails with @graphql-eslint/no-unused-variables
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:
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.CombinedGraphQLErrors, and CombinedProtocolErrors from the router): the backend has already reported it.UNAUTHENTICATED or UNAUTHORIZED, HTTP 401, or a subscription socket closed with 4401 or 4403): the client's auth.onUnauthorized deals with it.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.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.
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:
All operations must be named for better debugging and tooling:
Naming conventions:
Get or describe the data (e.g., GetUser, SearchIncidents)UpdateChatroom, CreateIncident)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.
For better UX, use optimistic updates with mutations:
Once you're comfortable writing operations, the next section covers where to organize them using the colocation pattern.
On this page