writing-graphql-operations
Creates GraphQL queries and mutations using gql.tada and urql. Implements repository pattern with type-safe operations and error handling. Triggers on: GraphQL query, GraphQL mutation, gql.tada, urql client, Saleor API, fragments, repository pattern, fetch-schema, MSW mocks.
allowed_tools: Read, Grep, Glob, Write, Edit
$ Installer
git clone https://github.com/saleor/configurator /tmp/configurator && cp -r /tmp/configurator/.claude/skills/writing-graphql-operations ~/.claude/skills/configurator// tip: Run this command in your terminal to install the skill
SKILL.md
name: writing-graphql-operations description: "Creates GraphQL queries and mutations using gql.tada and urql. Implements repository pattern with type-safe operations and error handling. Triggers on: GraphQL query, GraphQL mutation, gql.tada, urql client, Saleor API, fragments, repository pattern, fetch-schema, MSW mocks." allowed-tools: "Read, Grep, Glob, Write, Edit"
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
- File Organization
- Creating Operations with gql.tada
- Repository Pattern
- Error Handling
- Schema Management
- Testing GraphQL Operations
- Client Configuration
- Best Practices
- References
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.tadafor 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-docswith/urql-graphql/urql - gql.tada: Use
mcp__context7__resolve-library-idwith "gql.tada"
References
Skill Reference Files
- Error Handling - Error types, wrapping, and recovery patterns
- Fragment Patterns - Fragment composition and type inference
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-typesfor full implementation including bulk mutations - Bulk operations: See
adding-entity-types/references/bulk-mutations.mdfor chunking patterns - Testing GraphQL: See
analyzing-test-coveragefor MSW setup
Repository

saleor
Author
saleor/configurator/.claude/skills/writing-graphql-operations
20
Stars
2
Forks
Updated3d ago
Added6d ago