| name | graphql-api |
| description | WHAT: GraphQL queries and mutations with Apollo Client and code generation. WHEN: fetching GraphQL data, creating type-safe queries, composing fragments, testing Apollo. KEYWORDS: graphql, apollo, query, mutation, fragment, useQuery, useMutation, codegen, gql, mobile. |
GraphQL API Patterns with Apollo Client
Core Principles
Define queries in .graphql files and wrap them in custom hooks. Use code generation for TypeScript types, organize by domain, compose with fragments, and handle errors with errorPolicy.
Why: GraphQL with Apollo Client provides type-safe server state management with automatic caching, request deduplication, and optimistic updates. Code generation ensures TypeScript types stay in sync with the backend schema, preventing runtime errors.
When to Use This Skill
Use these patterns when:
- Fetching data from GraphQL APIs
- Building type-safe queries and mutations
- Organizing GraphQL operations by feature domain
- Reusing common data shapes with fragments
- Handling partial data and errors gracefully
- Testing components with GraphQL dependencies
- Generating TypeScript types from schema
- Managing server state with caching
File Organization
Domain-Based Structure
data-access/graphql/
├── store/
│ ├── GetStoreProducts.graphql # Query definition
│ ├── GetInitialStore.graphql # Complex query
│ ├── queries.ts # Custom hooks
│ └── index.ts # Public exports
├── cart/
│ ├── GetCartQuery.graphql # Query definition
│ ├── UpdateCartMutation.graphql # Mutation definition
│ ├── queries.ts # Query hooks
│ ├── mutations.ts # Mutation hooks
│ └── index.ts # Public exports
├── fragments/
│ ├── CartFragment.graphql # Reusable fragment
│ ├── ProductFragment.graphql # Reusable fragment
│ └── DeliveryFragment.graphql # Reusable fragment
└── README.md
Why: Domain-based organization keeps related GraphQL operations together, making them easier to find, maintain, and test. Fragment reuse prevents duplication and ensures consistency.
Production Example: git-resources/shared-mobile-modules/src/data-access/graphql/
Required Files Per Domain
domain/
├── OperationName.graphql # Query/mutation/subscription definition
├── queries.ts # Custom hooks for queries
├── mutations.ts # Custom hooks for mutations (if needed)
└── index.ts # Public exports only
Why: This structure enforces separation of concerns: .graphql files define operations, .ts files wrap them in hooks, index.ts controls the public API.
Query Definition
Basic Query in .graphql File
query GetStoreProducts(
$selectedWeek: WeekId!
$categoryId: CategoryId!
$filters: [FilterInput!]
) {
customer {
id
plans(type: RTE) {
id
items {
deliveries(
first: 1
filter: {
range: { start: $selectedWeek, size: 1 }
states: [PAUSED, UPCOMING]
}
) {
edges {
node {
menu {
products(
first: 100
filter: {
category: { categoryId: $categoryId }
filters: $filters
}
) {
edges {
node {
...ShoppableProductCardFragment
}
}
}
}
}
}
}
}
}
}
}
Why: Defining queries in .graphql files enables:
- IDE autocomplete with GraphQL plugins
- Automatic TypeScript type generation
- Schema validation at build time
- Syntax highlighting and error detection
Production Example: git-resources/shared-mobile-modules/src/data-access/graphql/store/GetStoreProducts.graphql
Naming Conventions
query GetStoreProducts { ... }
query GetInitialStoreData { ... }
mutation UpdateCart { ... }
mutation FavoriteProduct { ... }
query GetProductDetails { ... }
mutation CreateOrder { ... }
mutation DeleteRecipe { ... }
query GetStoreProducts(
$selectedWeek: WeekId!
$categoryId: CategoryId!
) { ... }
Why: Consistent naming makes operations discoverable and predictable. Code generation creates GetStoreProductsDocument and GetStoreProductsQuery types from operation names.
Custom Hooks Pattern
Wrapping Queries
import type { QueryHookOptions } from '@apollo/client';
import { useQuery } from '@apollo/client';
import { GetStoreProductsDocument } from '@data-access/graphql';
import type {
GetStoreProductsQuery,
GetStoreProductsQueryVariables,
} from '@data-access/graphql';
export const useGetStoreProductsQuery = (
options: QueryHookOptions<
GetStoreProductsQuery,
GetStoreProductsQueryVariables
>
) => {
return useQuery(GetStoreProductsDocument, options);
};
Why: Custom hooks provide a consistent API, enable additional processing (data extraction, error handling), and make operations easier to mock in tests.
Production Example: git-resources/shared-mobile-modules/src/data-access/graphql/store/queries.ts:226
Wrapping Mutations
import type { MutationHookOptions } from '@apollo/client';
import { useMutation } from '@apollo/client';
import { UpdateCartDocument } from '@data-access/graphql';
import type {
UpdateCartMutation,
UpdateCartMutationVariables,
} from '@data-access/graphql';
export const useUpdateCartMutation = (
options: MutationHookOptions<UpdateCartMutation, UpdateCartMutationVariables>
) => useMutation(UpdateCartDocument, options);
Why: Mutation hooks follow the same pattern as query hooks, providing consistency across the codebase and enabling easy testing.
Production Example: git-resources/shared-mobile-modules/src/data-access/graphql/cart/mutations.ts:10
Public Exports
export { useGetStoreProductsQuery } from './queries';
export { useGetInitialStoreQuery } from './queries';
export type { InitialStoreData } from './queries';
Why: Exporting through index.ts controls the public API, allowing internal refactoring without breaking consumers.
Fragment Composition
Defining Fragments
fragment CartFragment on Cart {
id
mealChoiceDone
selections {
...ProductSelectionFragment
}
actionOverrides {
disableAllActions
productOverrides {
productId
actionsOverrides {
type: __typename
allowed
reason
... on IncreaseQuantity {
step
}
... on DecreaseQuantity {
step
}
}
}
}
prices {
...ProductPricingFragment
}
config {
...ConfigFragment
}
}
fragment ProductSelectionFragment on ProductSelection {
id
legacyCourseIndex
quantity
pairedWith
subscription {
quantity
status
}
}
Why: Fragments define reusable data shapes, ensuring consistency across queries and reducing duplication. Fragment composition (nesting fragments) creates a hierarchy of reusable pieces.
Production Example: git-resources/shared-mobile-modules/src/data-access/graphql/fragments/CartFragment.graphql
Using Fragments in Queries
query GetStoreInitialData(
$selectedWeek: WeekId!
$startWeek: WeekId!
$categoryId: CategoryId!
) {
customer {
id
plans(type: RTE) {
id
items {
selectedDelivery: deliveries(
first: 1
filter: {
range: { start: $selectedWeek, size: 1 }
states: [PAUSED, UPCOMING]
}
) {
edges {
node {
...DeliveryFragment
menu {
categories {
...CategoryFragment
}
filters {
...FilterFragment
}
products(first: 100) {
edges {
node {
...ShoppableProductCardFragment
}
}
}
}
}
}
}
}
}
}
}
Why: Fragments keep queries readable and maintainable. When fragment definition changes, all queries using it automatically get the updated fields.
Production Example: git-resources/shared-mobile-modules/src/data-access/graphql/store/GetInitialStore.graphql
Data Processing Pattern
Extracting and Processing Data
import type { QueryHookOptions } from '@apollo/client';
import { useQuery } from '@apollo/client';
import { GetStoreInitialDataDocument } from '@data-access/graphql';
import type {
GetStoreInitialDataQuery,
GetStoreInitialDataQueryVariables,
DeliveryFragmentFragment,
} from '@data-access/graphql';
export const extractDeliveryList = (
data: GetStoreInitialDataQuery
): DeliveryFragmentFragment[] => {
const edges = data?.customer?.plans?.[0]?.items?.[0]?.deliveries?.edges;
if (!edges || !edges.length) {
return [];
}
return edges.reduce((acc, edge) => {
const delivery = edge?.node;
if (!delivery || delivery?.isPostCutOff) {
return acc;
}
return [...acc, delivery];
}, [] as DeliveryFragmentFragment[]);
};
export const useGetInitialStoreQuery = (
options: QueryHookOptions<
GetStoreInitialDataQuery,
GetStoreInitialDataQueryVariables
>
) => {
const actualQuery = useQuery(GetStoreInitialDataDocument, options);
const processedDeliveryList = actualQuery.data
? extractDeliveryList(actualQuery.data)
: undefined;
return {
...actualQuery,
data: actualQuery.error || actualQuery.loading
? undefined
: { processedDeliveryList },
};
};
Why: Processing raw GraphQL responses into domain-specific data shapes isolates complexity, provides type-safe access, and makes components easier to test.
Production Example: git-resources/shared-mobile-modules/src/data-access/graphql/store/queries.ts:156
Error Handling
Using errorPolicy
import { useQuery } from '@apollo/client';
import { GetCartDocument } from '@data-access/graphql';
export const useGetCartQuery = (options: QueryHookOptions) => {
return useQuery(GetCartDocument, {
...options,
errorPolicy: 'ignore',
});
};
Error Policy Options:
errorPolicy: 'none'
errorPolicy: 'ignore'
errorPolicy: 'all'
Why: errorPolicy: 'ignore' or 'all' enables graceful degradation when parts of a query fail. Users see available data instead of a blank screen.
Production Example: git-resources/shared-mobile-modules/src/data-access/graphql/cart/queries.ts:23
Conditional Error Handling
export const useGetStoreProductsQuery = (
options: QueryHookOptions<
GetStoreProductsQuery,
GetStoreProductsQueryVariables
>
) => {
const actualQuery = useQuery(GetStoreProductsDocument, options);
const processedData = actualQuery.data
? extractProducts(actualQuery.data)
: undefined;
if ((!processedData || processedData.length === 0) && actualQuery.error) {
return {
...actualQuery,
loading: actualQuery.networkStatus === NetworkStatus.refetch,
error: actualQuery.error,
data: processedData,
retry: actualQuery.refetch,
};
}
return {
...actualQuery,
error: undefined,
data: processedData,
loading: actualQuery.loading,
retry: actualQuery.refetch,
};
};
Why: Conditional error handling allows showing partial data when available, only treating it as an error when no data can be displayed.
Production Example: git-resources/shared-mobile-modules/src/data-access/graphql/store/queries.ts:226
Query Options
Common Query Options
import { useGetStoreProductsQuery } from '@data-access/graphql/store';
export const ProductList = () => {
const { data, loading, error, refetch } = useGetStoreProductsQuery({
variables: {
selectedWeek: '2024-W10',
categoryId: 'meals',
},
fetchPolicy: 'cache-first',
errorPolicy: 'all',
pollInterval: 30000,
skip: !selectedWeek,
onCompleted: (data) => {
console.log('Query completed', data);
},
onError: (error) => {
console.error('Query failed', error);
},
});
if (loading) return <LoadingSpinner />;
if (error) return <ErrorMessage error={error} />;
return <ProductGrid products={data} />;
};
Fetch Policy Options:
fetchPolicy: 'cache-first'
fetchPolicy: 'cache-only'
fetchPolicy: 'network-only'
fetchPolicy: 'no-cache'
fetchPolicy: 'cache-and-network'
Why: Fetch policies control cache behavior, enabling offline support, optimistic UI, and performance optimization.
useMemo for Variables
import { useMemo } from 'react';
import { useGetInitialStoreQuery } from '@data-access/graphql/store';
export const useInitialStoreDataLoader = ({
categoryId,
selectedWeek,
startWeek,
}: Props) => {
const memoizedVariables = useMemo(
() => ({
categoryId,
selectedWeek,
startWeek,
}),
[categoryId, selectedWeek, startWeek]
);
const result = useGetInitialStoreQuery({
variables: memoizedVariables,
fetchPolicy: 'cache-first',
});
return result;
};
Why: Memoizing variables prevents Apollo cache misses caused by new object references on every render, improving performance and preventing unnecessary network requests.
Production Example: git-resources/shared-mobile-modules/src/modules/store/screens/storefront/hooks/use-initial-store-data-loader/useInitialStoreDataLoader.ts:119
Mutations
Mutation Definition
mutation UpdateCart(
$planId: PlanId!
$deliveryId: DeliveryId!
$selectionInput: [UpdateSelectionInput!]!
$isSeamlessDowngradeEnabled: Boolean!
) {
updateCart(
planId: $planId
deliveryId: $deliveryId
selection: $selectionInput
isSeamlessDowngradeEnabled: $isSeamlessDowngradeEnabled
) {
errors {
productId
type
}
seamlessDowngraded
}
}
Production Example: git-resources/shared-mobile-modules/src/data-access/graphql/cart/UpdateCartMutation.graphql
Using Mutations
import { useUpdateCartMutation } from '@data-access/graphql/cart';
export const useCartActions = () => {
const [updateCart, { loading, error }] = useUpdateCartMutation({
onCompleted: (data) => {
if (data.updateCart.errors.length === 0) {
console.log('Cart updated successfully');
}
},
onError: (error) => {
console.error('Failed to update cart', error);
},
update: (cache, { data }) => {
cache.modify({
id: cache.identify({ __typename: 'Cart', id: cartId }),
fields: {
selections: () => data.updateCart.selections,
},
});
},
});
const handleAddToCart = async (productId: string, quantity: number) => {
try {
await updateCart({
variables: {
planId: currentPlanId,
deliveryId: currentDeliveryId,
selectionInput: [{ productId, quantity }],
isSeamlessDowngradeEnabled: true,
},
});
} catch (error) {
console.error(error);
}
};
return { handleAddToCart, loading, error };
};
Why: Mutations modify server state and can update the Apollo cache automatically. onCompleted and onError callbacks enable side effects like navigation or toast notifications.
Type Generation
Setup Code Generation
{
"scripts": {
"graphql:generate": "graphql-codegen --config codegen.yml",
"graphql:generate:watch": "graphql-codegen --config codegen.yml --watch"
}
}
overwrite: true
schema: 'https://api.example.com/graphql'
documents: 'src/data-access/graphql/**/*.graphql'
generates:
src/data-access/graphql/types.ts:
plugins:
- typescript
- typescript-operations
- typescript-react-apollo
config:
withHooks: true
withComponent: false
withHOC: false
Why: Code generation creates TypeScript types from .graphql files and the schema, ensuring type safety and preventing runtime errors from schema changes.
Generated Types
export type GetStoreProductsQueryVariables = Exact<{
selectedWeek: Scalars['WeekId'];
categoryId: Scalars['CategoryId'];
filters?: InputMaybe<Array<FilterInput> | FilterInput>;
}>;
export type GetStoreProductsQuery = {
__typename?: 'Query';
customer?: {
__typename?: 'Customer';
id: string;
plans: Array<{
__typename?: 'Plan';
id: string;
items: Array<{
__typename?: 'PlanItem';
deliveries: {
edges: Array<{
node: {
menu: {
products: {
edges: Array<{
node: ShoppableProductCardFragmentFragment;
}>;
};
};
};
}>;
};
}>;
}>;
};
};
export const GetStoreProductsDocument = gql`
query GetStoreProducts($selectedWeek: WeekId!, $categoryId: CategoryId!) {
customer {
id
plans(type: RTE) {
}
}
}
`;
Why: Generated types provide autocomplete, type checking, and prevent typos in query variables and response fields.
Testing
Mocking Apollo Client Hooks
import { useQuery } from '@apollo/client';
import { renderHook } from '@testing-library/react-native';
import { GetStoreProductsDocument } from '@data-access/graphql';
import type { ShoppableProductCardFragmentFragment } from '@data-access/graphql';
import { useGetStoreProductsQuery } from '../queries';
jest.mock('@apollo/client', () => ({
useQuery: jest.fn(),
NetworkStatus: {
ready: 7,
loading: 1,
error: 8,
},
}));
const mockUseQuery = useQuery as jest.Mock;
describe('useGetStoreProductsQuery', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should return extracted data when data is available', () => {
const mockProducts: ShoppableProductCardFragmentFragment[] = [
{ id: 'product-1', __typename: 'ShoppableProduct' },
{ id: 'product-2', __typename: 'ShoppableProduct' },
] as ShoppableProductCardFragmentFragment[];
const mockQueryResult = {
data: { products: mockProducts },
loading: false,
error: undefined,
networkStatus: 7,
refetch: jest.fn(),
};
mockUseQuery.mockReturnValue(mockQueryResult);
const { result } = renderHook(() =>
useGetStoreProductsQuery({
variables: { selectedWeek: '2023-W20' },
})
);
expect(mockUseQuery).toHaveBeenCalledWith(GetStoreProductsDocument, {
variables: { selectedWeek: '2023-W20' },
});
expect(result.current.data).toEqual(mockProducts);
expect(result.current.loading).toBe(false);
});
});
Why: Mocking Apollo hooks enables testing custom hook logic without real API calls, making tests fast and deterministic.
Production Example: git-resources/shared-mobile-modules/src/data-access/graphql/store/__tests__/useGetStoreProductsQuery.test.ts
Testing Loading and Error States
describe('useGetStoreProductsQuery', () => {
it('should return loading state', () => {
mockUseQuery.mockReturnValue({
data: undefined,
loading: true,
error: undefined,
networkStatus: 1,
refetch: jest.fn(),
});
const { result } = renderHook(() =>
useGetStoreProductsQuery({
variables: { selectedWeek: '2023-W20' },
})
);
expect(result.current.loading).toBe(true);
expect(result.current.data).toBeUndefined();
});
it('should return error state', () => {
const mockError = new Error('Network error');
mockUseQuery.mockReturnValue({
data: undefined,
loading: false,
error: mockError,
networkStatus: 8,
refetch: jest.fn(),
});
const { result } = renderHook(() =>
useGetStoreProductsQuery({
variables: { selectedWeek: '2023-W20' },
})
);
expect(result.current.error).toBe(mockError);
expect(result.current.loading).toBe(false);
});
});
Why: Testing loading and error states ensures components handle all query states correctly.
Common Mistakes to Avoid
❌ Don't define queries inline:
const { data } = useQuery(gql`
query GetProducts {
products {
id
name
}
}
`);
❌ Don't skip custom hooks:
import { useQuery } from '@apollo/client';
import { GetProductsDocument } from '@data-access/graphql';
export const ProductList = () => {
const { data } = useQuery(GetProductsDocument);
return <List items={data} />;
};
❌ Don't forget to memoize query variables:
export const ProductList = ({ categoryId }: Props) => {
const { data } = useGetStoreProductsQuery({
variables: { categoryId, selectedWeek: '2024-W10' },
});
return <List items={data} />;
};
❌ Don't ignore error handling:
export const ProductList = () => {
const { data } = useGetStoreProductsQuery({
variables: { selectedWeek: '2024-W10' },
});
return <List items={data} />;
};
✅ Do define queries in .graphql files:
query GetStoreProducts($categoryId: CategoryId!) {
products(categoryId: $categoryId) {
id
name
}
}
✅ Do use custom hooks:
import { useGetStoreProductsQuery } from '@data-access/graphql/store';
export const ProductList = () => {
const { data } = useGetStoreProductsQuery({
variables: { categoryId: 'meals' },
});
return <List items={data} />;
};
✅ Do memoize query variables:
import { useMemo } from 'react';
export const ProductList = ({ categoryId }: Props) => {
const variables = useMemo(
() => ({ categoryId, selectedWeek: '2024-W10' }),
[categoryId]
);
const { data } = useGetStoreProductsQuery({ variables });
return <List items={data} />;
};
✅ Do handle errors gracefully:
export const ProductList = () => {
const { data, loading, error, refetch } = useGetStoreProductsQuery({
variables: { selectedWeek: '2024-W10' },
errorPolicy: 'all',
});
if (loading) return <LoadingSpinner />;
if (error && !data) return <ErrorMessage error={error} onRetry={refetch} />;
return <List items={data || []} />;
};
Quick Reference
File Organization:
- Create domain folders in
data-access/graphql/
- Define operations in
.graphql files
- Wrap in custom hooks in
queries.ts/mutations.ts
- Export through
index.ts
- Store reusable fragments in
fragments/
Naming Conventions:
- Operations: PascalCase (GetStoreProducts, UpdateCart)
- Variables: camelCase ($selectedWeek, $categoryId)
- Hooks: use prefix (useGetStoreProductsQuery)
- Files: Match operation name (GetStoreProducts.graphql)
Code Generation:
- Run
yarn graphql:generate --watch during development
- Generates TypeScript types from schema + operations
- Creates
Document exports for Apollo Client
- Provides type-safe query/mutation variables
Custom Hooks:
- Wrap
useQuery and useMutation from Apollo Client
- Add data processing logic (extraction, transformation)
- Provide consistent error handling
- Enable easy testing with mocks
Error Handling:
errorPolicy: 'none' - No data on error (default)
errorPolicy: 'ignore' - Return data, ignore errors
errorPolicy: 'all' - Return both data and errors
- Use
onError callback for logging/tracking
Testing:
- Mock
useQuery and useMutation from @apollo/client
- Test loading, error, and success states
- Verify correct document and variables passed
- Use
renderHook for custom hook testing
Key Libraries:
- Apollo Client 3.13.6
- GraphQL Code Generator
- TypeScript 5.1.6
For production examples, see references/examples.md.