Axon 911 Frontend DocsAxon 911 Frontend Docs
Axon 911 Frontend Docs
    Loading navigation…
U
User
Switch to light mode
  1. Working With Data
  2. Graphql Development

GraphQL Development Workflow

Working with code generation, debugging, and troubleshooting

This guide covers the day-to-day development workflow when working with GraphQL, including code generation, debugging, and troubleshooting common issues.
Development Setup
Running Code Generation
pnpm dev and the shell variants (pnpm dev:responder, pnpm dev:console, pnpm dev:wallboard) run codegen once, when they start. It does not watch for changes. After you add or edit a .graphql file, regenerate the types:
Then restart the TypeScript server in your IDE if it does not pick up the new .graphql.ts files.
Working with Schema Changes
Updating the Schema
For frontends participating in the AxonMundi prototype, source the schema from its composed client schema rather than a single subgraph or local API endpoint. The established Prepared911 GraphQL stack remains the canonical integration for other frontends. Refresh the prototype schema before changing operations for a newly available backend field:
pnpm schema:sync uses the self-hosted Cosmo registry by default. It requires COSMO_API_URL and either your own pnpm schema:login session or approved CI credentials. For local, unmerged AxonMundi changes, set AXONMUNDI_DIR after running just compose in that checkout. See AxonMundi onboarding for access and credential boundaries, and Registry and schema workflow for the full refresh and drift-check process.
Generated Files
What Gets Generated
For each .graphql file, codegen creates a .graphql.ts file:
Generated File Contents
  1. Typed Document Nodes: For use with Apollo Client
  2. Query/Mutation Types: Full TypeScript types for results
  3. Variable Types: Type-safe variable interfaces
  4. Fragment Documents and Types: One fragment per component, read with useFragment; the types are masked
Git Configuration
Generated files are not committed to the repository:
This prevents:
  • Merge conflicts from generated code
  • Repository bloat
  • Outdated types if regeneration fails
Debugging GraphQL
TypeScript Type Issues
Problem: Types not updating after schema change
IntelliSense Not Working
If your IDE isn't showing proper types:
  1. Ensure codegen has run: Check for .graphql.ts files
  2. Restart TypeScript server:
    • VS Code: Cmd+Shift+P → "TypeScript: Restart TS Server"
    • Other IDEs: Check TypeScript service restart option
  3. Check imports: Ensure importing from .graphql (Vite resolves to .graphql.ts automatically)
Query Not Found
Error: Cannot find GetChatroomsDocument
Pre-commit Validation
Git hooks install with pnpm install. On commit, lint-staged runs the GraphQL checks on staged files: the layout check and graphql-eslint on .graphql files, and the Apollo oxlint rules on TypeScript. To run every check yourself:
GraphQL Lint Rules
pnpm lint:graphql is part of pnpm check, so CI runs it on every PR, uncached. It has three steps. The rules, their rationale, and what is deliberately off are in "GraphQL gate" in LINT-POLICY. Every rule is an error, and there is no suppressions file: fix the document.
1. Layout
packages/tooling-config/bin/check-graphql-layout.mjs fails when a .graphql file:
  • is outside apps/*/src, remotes/*/src, or packages/*/src (codegen would never read it, so it would get no generated types; a remote's exposes/ directory is not a document location);
  • is not named queries.graphql, mutations.graphql, subscriptions.graphql, or fragments.graphql;
  • holds a different kind of definition than its name says.
2. graphql-eslint on the documents
eslint.graphql.config.mjs checks each document against the vendored packages/data-gql/src/schema.graphql:
  • operations-recommended, including no-anonymous-operations, no-unused-variables, no-duplicate-fields, and the unique-* rules.
  • require-selections: select id where the type has one. This is what Apollo's cache normalization needs. A type without an id field is not affected.
  • axon/fragment-type-has-cache-identity: a fragment's type condition needs an id field or keyFields in packages/data-gql/src/cache-key-fields.json; on an interface or union, every type it can stand for does. See Types without an identity.
  • no-deprecated: do not select a deprecated field. Check the schema's deprecation reason for the replacement.
  • naming-convention: PascalCase operations, camelCase variables, no Query/Mutation/Subscription/Fragment suffix, and subscriptions start with On. A fragment is named Component_prop (ChatroomListItem_chatroom): the owning component in PascalCase, an underscore, then the prop in camelCase.
  • selection-set-depth: at most 7 levels. Fragments do not reduce it: the rule counts the fragments a document spreads. Seven fields are skipped because their shape is deep by nature: form, sections, questions (a hierarchical form), activeVersion (recursive versioning), and resource, v1, v2 (audit-log union shapes). The skip list is an escape hatch, not a fix: select less (drop levels the screen does not read, start nearer the data with a root field that takes an id, or split the query at the route), and add a field only if its schema shape is recursive or hierarchical by design (not merely because one query is deep), saying why in the PR. Never add a field only to make a lint error pass. The skip is by field name and applies in every document, and a test pins the list.
  • known-directives: accepts Apollo's client directives (@client, @connection, @nonreactive, @unmask). @unmask is an escape hatch: lint reports every use, and the way through is # eslint-disable-next-line no-restricted-syntax -- <why> on the line above.
Not enabled, on purpose: no-unused-fragments (it only sees spreads inside operations, so it flags a fragment that a component reads with useFragment, which is how every fragment is read under data masking, and it cannot see TypeScript), no-one-place-fragments (contradicts composing a child fragment into one parent), and the schema rules (the schema is vendored).
3. Apollo usage in TypeScript
An oxlint config, oxlint.graphql.config.ts, reports: inline gql (tests and stories may build a deliberately non-generated document), explicit type arguments on Apollo hooks (useQuery<Data, Vars>(...); the generated Document infers them), indexed access on a generated result type (GetThingQuery["thing"][0]; give the component its own fragment instead), imports of ./queries.graphql.ts instead of ./queries.graphql, fetch to a literal /graphql URL (with a no-restricted-syntax selector), and comparing an enum with a string (typescript/no-unsafe-enum-comparison, type-aware, so status === "ACTIVE" and case "ACTIVE": fail; compare with the member, ChatroomStatus.Active).
Related Documentation
  • Operations Guide - Writing queries, mutations, subscriptions
  • Fragments Guide - Using fragments
Troubleshooting Common Issues
Issue: "Cannot find module './queries.graphql'"
Cause: Codegen hasn't run yet Solution:
Issue: "Type '"ACTIVE"' is not assignable to type 'ChatroomStatus'"
Cause: Using string instead of enum Solution:
Issue: "Fragment 'UserData' not found"
Cause: No document defines a fragment with that name. Codegen and lint read every .graphql file under src/ of an app, remote, or package, so there is no import to add. Common reasons are a misspelled name (a fragment is named Component_prop, such as UserCard_user, not UserData), or a fragment in a file that isn't named fragments.graphql or isn't under a document location, which pnpm lint:graphql reports. Solution:
Issue: Slow Codegen Performance
Cause: Processing too many files Solutions:
  1. Ensure documents glob pattern is specific
  2. Exclude generated files from search
  3. Use more specific paths in codegen config
Testing GraphQL Components
Component tests run against a real Apollo client. renderWithApollo from @prepared911/util-testing builds the same client the app uses, including its batching link, and points it at MSW. The MSW handlers from @prepared911/graphql-mocks execute every operation against the vendored schema. An operation or field the schema does not have fails the test, so a mock can't drift from the API. The wiring and the reasons for it are in docs/TESTING.md under "GraphQL mocking". The rules for tests follow the rules for components:
  • Render the query root. The route, page or top-level view owns the request, so that is what a test renders. Its children get their data through fragments and don't call useQuery.
  • Assert on the output. Check what the user sees (text, roles, alerts), not the documents, the cache or the props.
  • Don't write mocks = [{ request, result }] arrays or wrap the component in MockedProvider. The real client talks to schema-driven handlers instead.
Setup
Add @prepared911/util-testing and @prepared911/graphql-mocks as dev dependencies, then use util-testing's setup file in the package's Vitest config:
The setup file starts an MSW server with the default schema-driven handlers, resets it after each test, and fails any operation that no handler matches, so a request never reaches the network. Building the mock schema takes a few seconds on a cold run. packages/util-testing/vitest.config.ts raises hookTimeout and testTimeout for that reason, and your package may need the same.
Test the Query Root
Most tests need no mock at all: the default handlers answer any operation in the schema with generated data. Pin data only for the values the test asserts on. Pass createPreparedMockHandlers to graphqlMockServer.use, and the override applies to that test and is reset afterwards.
mocks takes a function per type (DispatchCenter: () => ({ name: "Lakeside" }) applies to every dispatch center) or per field under Query, as above. Fields you leave out are generated. The test asserts on the rendered names, which come from a child component that reads its own fragment, so one test covers the root and its children together. Don't reach for a raw graphql.query("Name", …) from MSW. It skips schema validation, and it silently does not apply when the client batches requests. createPreparedMockHandlers covers both shapes.
Errors
Give a field a resolver that throws to test a failed query or mutation. The resolvers option replaces the field's generated result:
Mutations
Render the component that owns the useMutation, interact with it as a user would, and assert on what the user sees. Use a throwing resolver under Mutation for the failure path, and assert that the component shows the error:
A mutation that succeeds needs no override: the default handlers return a valid payload.
Testing a Fragment-Owning Component on Its Own
A component that takes a FragmentType prop can't be rendered with a plain object: it needs a reference that came out of a query. Today's sanctioned way is a small test-only query that selects the parent field and spreads the component's fragment, plus a probe component that runs that query and hands the component the result. renderWithApollo renders the probe, and the same MSW handlers supply the data. This is the shape of the probe in packages/util-testing/src/render.test.tsx. The test-only query goes in the component's queries.graphql, next to its fragments.graphql. Give it a name nothing else uses:
The test file holds the probe. It is a test, so it may call useQuery, and it passes the component data.allDispatchCenters[0], which is exactly what the real query root passes:
Notes on the recipe:
  • The probe's query spreads the component's own fragment and nothing else, so the test exercises exactly the fields the component declares. If you add a field to the fragment, the data for it is generated, and your test needs no change.
  • Don't import the parent's fragment or query into the test. The child's test depends on the child.
  • If the behavior you care about is what the user sees on the whole page, test the query root instead, as in "Test the Query Root". Use the probe when you want to exercise one component's fragment on its own.
  • To seed fragment data in the cache instead, Apollo's documented pattern is client.writeFragment({ fragment, fragmentName, data }) with from: { __typename, id }. A seedFragment helper (CORE-1305) is coming as a thin wrapper over it.
  • A helper that wraps the probe may come later (CORE-1301). Until then, this recipe is the supported way; don't write a private helper or a MockedProvider stand-in.
Temporary limitation: nested fragment spreads (until CORE-1303 lands)
For now, a generated …FragmentDoc does not include the fragments it spreads. A query that spreads several fragments works, because operation documents include them. But useFragment, cache.readFragment and cache.writeFragment on a fragment that itself spreads a child's fragment throw No fragment named <Child>, in tests and in production. Until CORE-1303 (#1238) lands, the component under test, and the recipe above, use a leaf fragment: one with no ...OtherFragment spread inside it. A route's query may spread any number of leaf fragments. This note goes away when CORE-1303 lands.
Testing Custom Hooks
A hook that calls an Apollo hook needs a client, and renderWithApollo renders elements, not hooks. Render a small probe component that calls the hook and prints what the test asserts on, the same way the probe above calls useQuery:
Best Practices
Do
  • Render the query root and assert on the output.
  • Pin only the values the test asserts on, with createPreparedMockHandlers, and let the schema generate the rest.
  • Use a throwing resolver to test that a failed query or mutation shows the user an error.
  • Test a fragment-owning component with the probe recipe above.
  • Use userEvent.setup() for interaction and role or text queries for assertions.
  • Import Documents from the colocated ./queries.graphql, and enums and inputs from @prepared911/data-gql/types.
Don't
  • Don't wrap components in MockedProvider or write { request, result } mock arrays.
  • Don't mock useQuery, useFragment or other Apollo hooks with vi.mock.
  • Don't hand a fragment-owning component a plain object cast to FragmentType. It passes the type check and hides whether the fragment and the query agree.
  • Don't assert on the cache, the generated Documents or the request body. Assert on what the user sees.
  • Don't call a query hook in a component under test to get around the probe. A component below the query root reads a fragment.
Related Documentation
  • Operations Guide - Writing queries and mutations
  • Fragments Guide - Component-owned fragments and data masking
  • docs/TESTING.md - Vitest wiring and GraphQL mocking
  • Apollo Client Testing - Apollo's own testing guide. It teaches MockedProvider, which we don't use, so apply only its general advice
Development Tips
1. Use GraphQL Extensions
Install GraphQL extensions for your IDE:
  • VS Code: Apollo GraphQL - Provides syntax highlighting, autocomplete, and validation for GraphQL files
  • IntelliJ: GraphQL plugin
2. Apollo Client Devtools
Install browser extension for debugging:
  • View cache contents
  • Inspect active queries
  • Test mutations manually
3. Type-First Development
  1. Write your GraphQL operation first
  2. Codegen automatically generates types (or run pnpm turbo run codegen manually)
  3. Use generated types in components
  4. Let TypeScript guide your implementation
4. Fragment Organization
A fragment belongs to one component. Keep it in a fragments.graphql next to that component, name it Component_prop, and spread it only from the component's parent:
5. Debugging Network Requests
Use Apollo Client's built-in logging:
Performance Considerations
Codegen Performance
To improve codegen speed:
  1. Specific globs: Use precise file patterns
  2. Exclude tests: Don't process test files
  3. Parallel generation: Codegen runs in parallel by default
Runtime Performance
  1. Give each component a fragment of only the fields it reads: the route's query then asks for what is rendered and no more
  2. Avoid deeply nested queries: Keep queries shallow and focused. Excessive nesting can lead to performance issues on both the client and server. See Depth limiting
Environment-Specific Configuration
Local Development
CI Environment
Summary
This guide covered the practical aspects of working with GraphQL in this monorepo. Here's a quick reference of key concepts:
Key Workflows
  • Code Generation: Runs once when pnpm dev starts; run pnpm turbo run codegen after editing a .graphql file
  • File Organization: Use colocation to keep .graphql files next to components
  • Type Safety: Import from .graphql files (Vite resolves to .graphql.ts automatically)
  • Debugging: Use Apollo DevTools, check generated files, restart TypeScript server
Quick Reference
  • Generate types: pnpm turbo run codegen
  • Check for generated files: Look for .graphql.ts files next to .graphql files
  • Fix IntelliSense: Restart TypeScript server in your IDE
  • Troubleshoot imports: Ensure codegen has run and files exist
Related Documentation
  • Operations Guide - Writing queries, mutations, subscriptions
  • Colocation Patterns - Organizing GraphQL files
  • Using Fragments - Type-safe data sharing
Additional Resources
  • Review Apollo Client documentation for advanced patterns
  • Learn about GraphQL best practices
  • Explore Apollo's developer tools
Previous
Working with data / GraphQL Fragments
Next
Working with data / GraphQL Cache
On this page
Development Setup
Running Code Generation
Working with Schema Changes
Updating the Schema
Generated Files
What Gets Generated
Generated File Contents
Git Configuration
Debugging GraphQL
TypeScript Type Issues
IntelliSense Not Working
Query Not Found
Pre-commit Validation
GraphQL Lint Rules
1. Layout
2. graphql-eslint on the documents
3. Apollo usage in TypeScript
Related Documentation
Troubleshooting Common Issues
Issue: "Cannot find module './queries.graphql'"
Issue: "Type '"ACTIVE"' is not assignable to type 'ChatroomStatus'"
Issue: "Fragment 'UserData' not found"
Issue: Slow Codegen Performance
Testing GraphQL Components
Setup
Test the Query Root
Errors
Mutations
Testing a Fragment-Owning Component on Its Own
Temporary limitation: nested fragment spreads (until CORE-1303 lands)
Testing Custom Hooks
Best Practices
Do
Don't
Related Documentation
Development Tips
1. Use GraphQL Extensions
2. Apollo Client Devtools
3. Type-First Development
4. Fragment Organization
5. Debugging Network Requests
Performance Considerations
Codegen Performance
Runtime Performance
Environment-Specific Configuration
Local Development
CI Environment
Summary
Key Workflows
Quick Reference
Related Documentation
Additional Resources