GraphQL Operations Developer
Purpose
Guide the creation and maintenance of GraphQL operations following project conventions for type safety, organization, error handling, and testing.
When to Use
- Creating new GraphQL queries or mutations
- Integrating with Saleor API endpoints
- Updating schema after Saleor changes
- Working with urql client configuration
- Creating mocks for testing
Table of Contents
Project GraphQL Stack
| Tool |
Purpose |
| urql |
GraphQL client with caching |
| gql.tada |
Type-safe GraphQL with TypeScript inference |
| @urql/exchange-auth |
Authentication handling |
| @urql/exchange-retry |
Rate limiting and retry logic |
File Organization
src/lib/graphql/
├── client.ts # urql client configuration
├── operations/ # GraphQL operation definitions
│ ├── products.ts
│ ├── categories.ts
│ └── ...
├── fragments/ # Reusable GraphQL fragments
│ └── ...
├── __mocks__/ # Test mocks
│ └── ...
└── schema.graphql # Saleor schema (generated)
src/modules/<entity>/
├── repository.ts # Uses GraphQL operations
└── ...
Creating Operations with gql.tada
Basic Query
import { graphql } from 'gql.tada';
// gql.tada infers types from schema automatically
export const GetCategoriesQuery = graphql(`
query GetCategories($first: Int!) {
categories(first: $first) {
edges {
node {
id
name
slug
description
parent {
id
slug
}
}
}
}
}
`);
// Type is inferred automatically
type GetCategoriesResult = ResultOf<typeof GetCategoriesQuery>;
Query with Variables
export const GetCategoryBySlugQuery = graphql(`
query GetCategoryBySlug($slug: String!) {
category(slug: $slug) {
id
name
slug
description
level
parent {
id
slug
}
children(first: 100) {
edges {
node {
id
name
slug
}
}
}
}
}
`);
Mutation
export const CreateCategoryMutation = graphql(`
mutation CreateCategory($input: CategoryInput!) {
categoryCreate(input: $input) {
category {
id
name
slug
}
errors {
field
message
code
}
}
}
`);
Using Fragments
// Define reusable fragment
export const CategoryFieldsFragment = graphql(`
fragment CategoryFields on Category {
id
name
slug
description
level
}
`);
// Use in query
export const GetCategoriesWithFragmentQuery = graphql(`
query GetCategoriesWithFragment($first: Int!) {
categories(first: $first) {
edges {
node {
...CategoryFields
}
}
}
}
`, [CategoryFieldsFragment]);
Repository Pattern
Standard Repository Structure
// src/modules/category/repository.ts
import { Client } from '@urql/core';
import { graphql } from 'gql.tada';
import { GraphQLError } from '@/lib/errors';
const GetCategoriesQuery = graphql(`...`);
const CreateCategoryMutation = graphql(`...`);
export class CategoryRepository {
constructor(private readonly client: Client) {}
async findAll(): Promise<Category[]> {
const result = await this.client.query(GetCategoriesQuery, { first: 100 });
if (result.error) {
throw GraphQLError.fromCombinedError(result.error, 'GetCategories');
}
return this.mapCategories(result.data?.categories);
}
async create(input: CategoryInput): Promise<Category> {
const result = await this.client.mutation(CreateCategoryMutation, { input });
if (result.error) {
throw GraphQLError.fromCombinedError(result.error, 'CreateCategory');
}
if (result.data?.categoryCreate?.errors?.length) {
throw new GraphQLError(
'Category creation failed',
result.data.categoryCreate.errors
);
}
return this.mapCategory(result.data?.categoryCreate?.category);
}
// Map GraphQL response to domain model
private mapCategory(gqlCategory: GqlCategory | null): Category {
if (!gqlCategory) {
throw new EntityNotFoundError('Category not found');
}
return {
id: gqlCategory.id,
name: gqlCategory.name,
slug: gqlCategory.slug,
description: gqlCategory.description ?? undefined,
};
}
}
Error Handling
Wrapping GraphQL Errors
import { CombinedError } from '@urql/core';
import { GraphQLError } from '@/lib/errors';
// In repository methods
if (result.error) {
throw GraphQLError.fromCombinedError(
result.error,
'OperationName',
{ entitySlug: input.slug } // Additional context
);
}
// Handle mutation-specific errors
if (result.data?.mutationName?.errors?.length) {
const errors = result.data.mutationName.errors;
throw new GraphQLError(
`Operation failed: ${errors.map(e => e.message).join(', ')}`,
errors
);
}
Common Error Scenarios
| Error Type |
Cause |
Handling |
CombinedError |
Network/GraphQL errors |
GraphQLError.fromCombinedError() |
errors array |
Mutation validation errors |
Check result.data?.mutation?.errors |
| Rate limit (429) |
Too many requests |
Automatic retry via exchange |
| Auth error |
Invalid/expired token |
Re-authenticate |
Schema Management
Updating Schema
# Fetch latest schema from Saleor instance
pnpm fetch-schema
# This updates:
# - src/lib/graphql/schema.graphql
# - graphql-env.d.ts (type definitions)
When to Update Schema
- New Saleor features needed
- After Saleor version upgrade
- When encountering schema drift errors
- Commit schema changes with feature implementation
Testing GraphQL Operations
Mock Setup with MSW
// src/lib/graphql/__mocks__/category-mocks.ts
import { graphql, HttpResponse } from 'msw';
export const categoryHandlers = [
graphql.query('GetCategories', () => {
return HttpResponse.json({
data: {
categories: {
edges: [
{
node: {
id: 'cat-1',
name: 'Electronics',
slug: 'electronics',
},
},
],
},
},
});
}),
graphql.mutation('CreateCategory', ({ variables }) => {
return HttpResponse.json({
data: {
categoryCreate: {
category: {
id: 'new-cat',
name: variables.input.name,
slug: variables.input.slug,
},
errors: [],
},
},
});
}),
];
Repository Test Pattern
describe('CategoryRepository', () => {
const server = setupServer(...categoryHandlers);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
it('should fetch all categories', async () => {
const repository = new CategoryRepository(testClient);
const categories = await repository.findAll();
expect(categories).toHaveLength(1);
expect(categories[0].slug).toBe('electronics');
});
});
Client Configuration
urql Client Setup
// src/lib/graphql/client.ts
import { Client, cacheExchange, fetchExchange } from '@urql/core';
import { authExchange } from '@urql/exchange-auth';
import { retryExchange } from '@urql/exchange-retry';
export const createClient = (url: string, token: string) => {
return new Client({
url,
exchanges: [
cacheExchange,
authExchange(async utils => ({
addAuthToOperation(operation) {
return utils.appendHeaders(operation, {
Authorization: `Bearer ${token}`,
});
},
})),
retryExchange({
initialDelayMs: 1000,
maxDelayMs: 15000,
maxNumberAttempts: 5,
retryIf: error => {
// Retry on rate limits and network errors
return error?.response?.status === 429 || !error?.response;
},
}),
fetchExchange,
],
});
};
Best Practices
Do's
- Use
gql.tada for all operations (type inference)
- Keep operations close to their domain modules
- Map GraphQL responses to domain models
- Include operation name in error context
- Update mocks when schema changes
Don'ts
- Don't use raw string queries (no type safety)
- Don't expose GraphQL types directly to services
- Don't skip error handling for any operation
- Don't hardcode pagination limits (use constants)
Validation Checkpoints
| Phase |
Validate |
Command |
| Schema fresh |
No drift |
pnpm fetch-schema |
| Operations typed |
gql.tada inference |
Check IDE types |
| Mocks match |
MSW handlers |
pnpm test |
| Error handling |
All paths covered |
Code review |
Common Mistakes
| Mistake |
Issue |
Fix |
Not checking errors array |
Silent failures |
Always check result.data?.mutation?.errors |
| Exposing GraphQL types |
Coupling |
Map to domain types in repository |
| Missing error context |
Hard to debug |
Include operation name in errors |
| Stale schema |
Type errors |
Run pnpm fetch-schema after Saleor updates |
| Not using fragments |
Code duplication |
Extract shared fields to fragments |
External Documentation
For up-to-date library docs, use Context7 MCP:
- urql:
mcp__context7__get-library-docs with /urql-graphql/urql
- gql.tada: Use
mcp__context7__resolve-library-id with "gql.tada"
References
Skill Reference Files
Project Resources
{baseDir}/src/lib/graphql/client.ts - Client configuration
{baseDir}/src/lib/graphql/operations/ - Existing operations
{baseDir}/docs/CODE_QUALITY.md#graphql--external-integrations - Quality standards
Related Skills
- Complete entity workflow: See
adding-entity-types for full implementation including bulk mutations
- Bulk operations: See
adding-entity-types/references/bulk-mutations.md for chunking patterns
- Testing GraphQL: See
analyzing-test-coverage for MSW setup