🎯

writing-graphql-operations

🎯Skill

from saleor/configurator

VibeIndex|
What it does

Generates type-safe GraphQL operations using gql.tada and urql, implementing a repository pattern for Saleor API interactions with robust error handling.

writing-graphql-operations

Installation

Install skill:
npx skills add https://github.com/saleor/configurator --skill writing-graphql-operations
6
AddedJan 27, 2026

Skill Details

SKILL.md

"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."

Overview

# 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](#project-graphql-stack)
  • [File Organization](#file-organization)
  • [Creating Operations with gql.tada](#creating-operations-with-gqltada)
  • [Repository Pattern](#repository-pattern)
  • [Error Handling](#error-handling)
  • [Schema Management](#schema-management)
  • [Testing GraphQL Operations](#testing-graphql-operations)
  • [Client Configuration](#client-configuration)
  • [Best Practices](#best-practices)
  • [References](#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//

β”œβ”€β”€ repository.ts # Uses GraphQL operations

└── ...

```

Creating Operations with gql.tada

Basic Query

```typescript

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;

```

Query with Variables

```typescript

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

```typescript

export const CreateCategoryMutation = graphql(`

mutation CreateCategory($input: CategoryInput!) {

categoryCreate(input: $input) {

category {

id

name

slug

}

errors {

field

message

code

}

}

}

`);

```

Using Fragments

```typescript

// 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

```typescript

// 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 {

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 {

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

```typescript

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

```bash

# 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

```typescript

// 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

```typescript

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

```typescript

// 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

  • [Error Handling](references/error-handling.md) - Error types, wrapping, and recovery patterns
  • [Fragment Patterns](references/fragment-patterns.md) - 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-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

Quick Reference Rule

For a condensed quick reference, see .claude/rules/graphql-patterns.md (automatically loaded when editing GraphQL operations and repository files).

More from this repository8

🎯
implementing-cli-patterns🎯Skill

Here's a concise, practical description for the "implementing-cli-patterns" skill: Implements consistent CLI user experiences with color-coded output, progress indicators, interactive prompts, and...

🎯
creating-changesets🎯Skill

Automates changeset creation for semantic versioning, determining version bump type and generating detailed release notes.

🎯
reviewing-typescript-code🎯Skill

Reviews TypeScript code for type safety, clean code principles, functional patterns, Zod validation, and error handling across various project scenarios.

🎯
designing-zod-schemas🎯Skill

Designs and validates Zod schemas with type inference, supporting complex validation patterns, branded types, and transformations.

🎯
understanding-saleor-domain🎯Skill

Explains Saleor e-commerce domain, revealing entity identification, deployment pipeline, and configuration management strategies for developers.

🎯
analyzing-test-coverage🎯Skill

Analyzes and generates comprehensive tests using Vitest and MSW, creating test builders, mocking repositories, and configuring integration tests.

🎯
managing-github-ci🎯Skill

Configures and manages GitHub Actions workflows, automates releases via Changesets, validates PRs, and troubleshoots CI pipeline failures.

🎯
validating-pre-commit🎯Skill

Validates code quality by running linting, TypeScript compilation, tests, and CI checks before committing or pushing changes.