Prepared Docs

Prepared Docs

⌘K

    Loading navigation…

User

  1. Working With Data
  2. Graphql Operations

GraphQL Operations

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 with MockedProvider
  • Better IDE support and debugging
  • Type-safe variables with enums

Working with Enums

GraphQL enums are generated as TypeScript const enums, providing type safety for all enum values.

Using Generated Enums

❌ Don't do this - Magic Strings

✅ Do this - Type-Safe Enums

Enum Benefits with enumsAsConst

Our codegen configuration uses enumsAsConst: true, which generates:

This provides:

  • Tree-shakeable code (only used values are bundled)
  • Better TypeScript inference
  • Autocomplete in IDEs
  • Compile-time validation

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

Issues:

  • Fragile: Query structure changes break component types
  • Hard to discover: Types are hidden in query result structure
  • No owner: nothing says which component reads which field
  • Poor semantics: Type path doesn't describe its purpose
  • Nullability issues: Doesn't handle nullable query results safely

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.
  • 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.

Previous

Working with data / Pub/Sub

Next

Working with data / GraphQL Colocation

On this page

Queries
Basic Query Structure
Using in Components
Before and After: Hooks vs Document Nodes
❌ Don't do this - Auto-generated Hooks
✅ Do this - Typed Document Nodes
Working with Enums
Using Generated Enums
❌ Don't do this - Magic Strings
✅ Do this - Type-Safe Enums
Enum Benefits with enumsAsConst
Mutations
Basic Mutation
Using Mutations
Field Selection Best Practices
❌ Don't do this - Over-fetching
✅ Do this - Request Only What You Need
❌ Don't do this - Query Type Extractions
Variable Usage
❌ Don't do this - Unused Variables
✅ Do this - Use All Declared Variables
Subscriptions
Basic Subscription
Using Subscriptions
Error Handling
Who reports an error
Query Error Handling
Mutation Error Handling
Operation Naming Conventions
Optimistic Updates
Best Practices Summary
✅ Do
❌ Don't