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.
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.
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.
For each .graphql file, codegen creates a .graphql.ts file:
Generated files are not committed to the repository:
This prevents:
Problem: Types not updating after schema change
If your IDE isn't showing proper types:
.graphql.ts files.graphql (Vite resolves to .graphql.ts automatically)Error: Cannot find GetChatroomsDocument
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:
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.
packages/tooling-config/bin/check-graphql-layout.mjs fails when a .graphql file:
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);queries.graphql, mutations.graphql, subscriptions.graphql, or fragments.graphql;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.no-deprecated: do not select a deprecated field. Check the schema's deprecation reason for the replacement.naming-convention: PascalCase operations and fragments, camelCase variables, no Query/Mutation/Subscription/Fragment suffix, and subscriptions start with On.selection-set-depth: at most 7 levels. Use fragments to flatten a deep selection. 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: flatten the query with fragments first, 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).Not enabled, on purpose: no-unused-fragments (it flags a fragment read only with useFragment, which is supported, and cannot see TypeScript), no-one-place-fragments (contradicts composing a child fragment into one parent), and the schema rules (the schema is vendored).
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]; select a named fragment instead), aliased or namespace imports of Apollo hooks, and imports of ./queries.graphql.ts instead of ./queries.graphql.
Cause: Codegen hasn't run yet
Solution:
Cause: Using string instead of enum
Solution:
Cause: Missing fragment import in GraphQL file
Solution:
Cause: Processing too many files
Solutions:
documents glob pattern is specificFactory functions are exported from @prepared911/data-gql/factories, and document nodes are exported from @prepared911/data-gql:
Factory functions accept an optional overrides parameter to customize specific fields:
The most common use case is mocking GraphQL queries in component tests. Use renderWithMockedProvider from ~src/tests instead of manually setting up MockedProvider:
For testing custom hooks that use Apollo Client hooks (useQuery, useMutation, useSubscription), use renderHook from @testing-library/react with WithMockedProvider as the wrapper:
Key points for hook testing:
WithMockedProvider is a function that returns a component wrapper: wrapper: WithMockedProvider({ mocks, addTypename: false })You can override any field, including nested relationships:
Factory functions work seamlessly with fragment types. When you use a fragment in a query, you can create mock data that matches the fragment structure:
When mocking query results, structure your mock data to match the query's return type:
Note: The test setup uses a fixed Faker seed (faker.seed(0)), which means factory-generated mock data will be consistent across test runs. When you don't override specific fields, the factories will generate the same random values every time, making your tests more predictable and easier to debug.
Mutations work the same way as queries. Use factories for mutation payload types:
Subscriptions can be mocked the same way as queries and mutations:
renderWithMockedProvider: Use the helper from ~src/tests instead of manually setting up MockedProvideraChatroomRequestMediaDownloadPayload for mutation/subscription payloads__typename: The factories add this automatically, but ensure addTypename is set correctly in MockedProvidervi.mock("@prepared911/data-gql") and use typed-document-node mocks insteadHere's a complete example showing how to use factories in a real test with multiple operations and additional providers:
Key points from this example:
.graphql module (./queries.graphql), not from @prepared911/data-gql; enums, inputs, and factories come from @prepared911/data-gql/types and /factoriesaChatroomRequestMediaDownloadPayload)expect.any(String) or expect.any(Object) for variable matching when exact values aren't neededrenderWithMockedProvider with additional providers arrayThe codebase includes helper functions that combine factories with MockedProvider. Import from ~src/tests:
Common additional providers:
WithUiCoreProviders - Provides theme, resize observer, and base providersModalContextProvider - Provides modal contextThe renderWithMockedProvider helper automatically:
MockedProvider with addTypename={false}Install GraphQL extensions for your IDE:
Install browser extension for debugging:
pnpm turbo run codegen manually)Keep fragments close to their consumers:
Use Apollo Client's built-in logging:
To improve codegen speed:
This guide covered the practical aspects of working with GraphQL in this monorepo. Here's a quick reference of key concepts:
pnpm dev starts; run pnpm turbo run codegen after editing a .graphql file.graphql files next to components.graphql files (Vite resolves to .graphql.ts automatically)pnpm turbo run codegen.graphql.ts files next to .graphql filesOn this page