Генерация из GraphQL схемы

Интеграция GraphQL с RTK Query позволяет автоматически создавать типизированные endpoints на основе схемы GraphQL. Такой подход избавляет от ручного описания большого количества запросов, мутаций и типов, особенно в крупных проектах с десятками сущностей.

Автогенерация особенно полезна в следующих случаях:

  • backend уже предоставляет GraphQL API;
  • схема активно развивается;
  • требуется строгая типизация;
  • используется TypeScript;
  • необходимо сократить количество шаблонного кода;
  • проект содержит большое количество query и mutation операций.

Архитектура генерации

Типичная схема генерации выглядит следующим образом:

GraphQL Schema
        ↓
GraphQL Code Generator
        ↓
RTK Query endpoints
        ↓
React hooks
        ↓
Использование в компонентах

В основе процесса лежат три инструмента:

Инструмент Назначение
GraphQL Schema Описание API
GraphQL Code Generator Генерация кода
RTK Query Выполнение запросов и кеширование

Установка зависимостей

Для генерации RTK Query API обычно устанавливаются следующие пакеты:

npm install @reduxjs/toolkit graphql

Пакеты генерации:

npm install -D @graphql-codegen/cli

Плагины:

npm install -D \
@graphql-codegen/typescript \
@graphql-codegen/typescript-operations \
@graphql-codegen/typescript-rtk-query

Если используется schema introspection через URL:

npm install -D cross-fetch

Структура проекта

Пример структуры:

src/
├── app/
│   └── store.ts
│
├── services/
│   ├── api.ts
│   ├── generated.ts
│   └── graphql/
│       ├── queries.graphql
│       └── mutations.graphql
│
├── codegen.yml
└── package.json

GraphQL схема

Пример схемы:

type User {
  id: ID!
  name: String!
  email: String!
}

type Query {
  users: [User!]!
  user(id: ID!): User
}

type Mutation {
  createUser(name: String!, email: String!): User!
}

Базовый API RTK Query

RTK Query требует базового API, в который позже будут инжектироваться endpoints.

// services/api.ts

import { createApi } from '@reduxjs/toolkit/query/react'
import { graphqlRequestBaseQuery } from '@rtk-query/graphql-request-base-query'

export const api = createApi({
    reducerPath: 'api',

    baseQuery: graphqlRequestBaseQuery({
        url: 'https://api.example.com/graphql',
    }),

    endpoints: () => ({}),
})

Установка GraphQL baseQuery

Для GraphQL чаще всего используется:

npm install graphql-request
npm install @rtk-query/graphql-request-base-query

GraphQL операции

queries.graphql

query GetUsers {
  users {
    id
    name
    email
  }
}

query GetUser($id: ID!) {
  user(id: $id) {
    id
    name
    email
  }
}

mutations.graphql

mutation CreateUser($name: String!, $email: String!) {
  createUser(name: $name, email: $email) {
    id
    name
    email
  }
}

Конфигурация GraphQL Code Generator

codegen.yml

schema: https://api.example.com/graphql

documents:
  - "./src/services/graphql/**/*.graphql"

generates:
  ./src/services/generated.ts:
    plugins:
      - typescript
      - typescript-operations
      - typescript-rtk-query

    config:
      importBaseApiFrom: './api'
      exportHooks: true
      overrideExisting: true

Назначение плагинов

typescript

Генерирует базовые типы GraphQL.

Пример:

export type User = {
    id: string
    name: string
    email: string
}

typescript-operations

Создает типы для query и mutation операций.

Пример:

export type GetUsersQuery = {
    users: User[]
}

typescript-rtk-query

Создает RTK Query endpoints и React hooks.

Пример:

export const injectedRtkApi = api.injectEndpoints({
    endpoints: (build) => ({
        GetUsers: build.query<GetUsersQuery, GetUsersQueryVariables>({
            query: (variables) => ({
                document: GetUsersDocument,
                variables,
            }),
        }),
    }),
})

Генерация кода

Запуск генерации:

npx graphql-codegen

После выполнения появится файл:

src/services/generated.ts

Содержимое generated.ts

Сгенерированный файл обычно содержит:

  • GraphQL типы;
  • query types;
  • mutation types;
  • GraphQL documents;
  • RTK Query endpoints;
  • React hooks.

Автоматически созданные hooks

Code Generator создает hooks автоматически:

export const {
    useGetUsersQuery,
    useGetUserQuery,
    useCreateUserMutation,
} = injectedRtkApi

Использование query hooks

import { useGetUsersQuery } from '@/services/generated'

export function UsersPage() {
    const { data, isLoading } = useGetUsersQuery()

    if (isLoading) {
        return <div>Loading...</div>
    }

    return (
        <div>
            {data?.users.map((user) => (
                <div key={user.id}>
                    {user.name}
                </div>
            ))}
        </div>
    )
}

Использование mutation hooks

import { useCreateUserMutation } from '@/services/generated'

export function CreateUserForm() {
    const [createUser, { isLoading }] =
        useCreateUserMutation()

    const handleSubmit = async () => {
        await createUser({
            name: 'Alex',
            email: 'alex@example.com',
        })
    }

    return (
        <button onCl ick={handleSubmit}>
            Create
        </button>
    )
}

Типизация переменных

Генерация автоматически создает типы аргументов:

export type GetUserQueryVariables = Exact<{
    id: Scalars['ID']
}>

Это исключает ошибки передачи неправильных параметров.


Типизация ответа

Ответ query полностью типизирован:

const { data } = useGetUserQuery({
    id: '1',
})

data?.user?.email

IDE автоматически подсказывает поля.


Использование fragments

Fragments уменьшают дублирование.

user.fragment.graphql

fragment UserFields on User {
  id
  name
  email
}

query

query GetUsers {
  users {
    ...UserFields
  }
}

mutation

mutation CreateUser($name: String!, $email: String!) {
  createUser(name: $name, email: $email) {
    ...UserFields
  }
}

Генерация fragment types

Code Generator автоматически создает:

export type UserFieldsFragment = {
    id: string
    name: string
    email: string
}

Работа с authorization

Передача токена

export const api = createApi({
    reducerPath: 'api',

    baseQuery: graphqlRequestBaseQuery({
        url: 'https://api.example.com/graphql',

        prepareHeaders: (headers) => {
            const token = localStorage.getItem('token')

            if (token) {
                headers.set(
                    'authorization',
                    `Bearer ${token}`
                )
            }

            return headers
        },
    }),

    endpoints: () => ({}),
})

Генерация при локальной схеме

Вместо URL можно использовать локальный файл:

schema: ./schema.graphql

Генерация из introspection schema

Сначала можно скачать схему:

npx graphql-codegen introspection

После этого:

schema: ./schema.json

Автоматический watch режим

Для постоянной генерации:

npx graphql-codegen --watch

Это удобно при активной разработке backend схемы.


Инъекция endpoints

RTK Query использует injectEndpoints.

Сгенерированный код обычно выглядит так:

export const injectedRtkApi =
    api.injectEndpoints({
        endpoints: (build) => ({
            GetUsers: build.query({
                query: () => ({
                    document: GetUsersDocument,
                }),
            }),
        }),
    })

Такой подход позволяет:

  • разделять API по модулям;
  • избегать огромного createApi файла;
  • динамически подключать endpoints.

Настройка namingConvention

GraphQL Code Generator поддерживает преобразование имен.

config:
  namingConvention:
    typeNames: change-case-all#pascalCase
    enumValues: change-case-all#upperCase

Генерация хуков без React

Если React hooks не нужны:

config:
  exportHooks: false

Добавление transformResponse

Иногда требуется преобразование данных.

Можно расширить generated endpoints:

const extendedApi = injectedRtkApi.enhanceEndpoints({
    endpoints: {
        GetUsers: {
            transformResponse: (response) => {
                return response.users
            },
        },
    },
})

Использование scalar mappings

GraphQL Scalars могут быть сопоставлены с TypeScript типами.

GraphQL

scalar DateTime

codegen.yml

config:
  scalars:
    DateTime: string

Расширенное scalar mapping

config:
  scalars:
    DateTime: Date
    JSON: Record<string, unknown>

Генерация enum типов

GraphQL enum:

enum UserRole {
  ADMIN
  USER
}

Code Generator:

export enum UserRole {
    Admin = 'ADMIN',
    User = 'USER',
}

Работа с union types

GraphQL:

union SearchResult = User | Post

Generated types:

export type SearchResult =
    | User
    | Post

Inline fragments

query Search {
  search {
    __typename

    ... on User {
      id
      name
    }

    ... on Post {
      id
      title
    }
  }
}

Типизация inline fragments

Code Generator создает discriminated unions:

if (item.__typename === 'User') {
    item.email
}

Оптимизация generated файлов

В больших проектах generated.ts может стать огромным.

Распространенный подход:

generates:
  ./src/services/types.ts:
    plugins:
      - typescript

  ./src/services/:
    preset: near-operation-file

    presetConfig:
      extension: .generated.ts
      baseTypesPath: types.ts

    plugins:
      - typescript-operations
      - typescript-rtk-query

near-operation-file preset

Структура:

graphql/
├── users.query.graphql
├── users.query.generated.ts
├── posts.query.graphql
└── posts.query.generated.ts

Преимущества:

  • меньше merge conflicts;
  • лучшая модульность;
  • более быстрые изменения;
  • удобная навигация.

Генерация SDK

Дополнительно можно генерировать SDK:

plugins:
  - typescript
  - typescript-operations
  - typescript-graphql-request

Совмещение REST и GraphQL

RTK Query позволяет использовать одновременно:

  • REST endpoints;
  • GraphQL endpoints.

Пример:

export const api = createApi({
    baseQuery: fetchBaseQuery({
        baseUrl: '/api',
    }),

    endpoints: () => ({}),
})

Отдельный GraphQL API:

export const graphqlApi = createApi({
    baseQuery: graphqlRequestBaseQuery({
        url: '/graphql',
    }),

    endpoints: () => ({}),
})

Генерация tags

RTK Query поддерживает автоматическую invalidation систему.

Конфигурация

config:
  addTagTypes: true

Generated endpoint:

providesTags: ['User']

Mutation:

invalidatesTags: ['User']

Кастомизация generated endpoints

Code Generator поддерживает:

config:
  exposeQueryKeys: true
  exposeDocument: true
  exposeMutationKeys: true

exposeDocument

Позволяет получить GraphQL document:

GetUsersDocument

Это полезно для:

  • SSR;
  • prefetch;
  • тестирования;
  • Apollo interoperability.

SSR и Next.js

Сгенерированные endpoints можно использовать в SSR.

Пример prefetch:

store.dispatch(
    api.endpoints.GetUsers.initiate()
)

Code splitting

RTK Query поддерживает lazy loading endpoints.

const usersApi = api.injectEndpoints({
    endpoints: (build) => ({
        GetUsers: build.query({
            query: () => ({
                document: GetUsersDocument,
            }),
        }),
    }),
})

Автоматизация через package.json

{
  "scripts": {
    "codegen": "graphql-codegen",
    "codegen:watch": "graphql-codegen --watch"
  }
}

Генерация в CI/CD

Codegen часто запускается:

  • перед build;
  • перед typecheck;
  • в precommit hooks;
  • в CI pipeline.

Пример:

{
  "scripts": {
    "build": "npm run codegen && vite build"
  }
}

Проверка изменений схемы

GraphQL schema drift может ломать frontend.

Популярная практика:

graphql-inspector diff

Типичные проблемы

Отсутствие documents

Ошибка:

Unable to find any GraphQL type definitions

Причина:

documents:
  - "./src/**/*.graphql"

не находит файлы.


Ошибка introspection

Причины:

  • backend недоступен;
  • CORS;
  • отсутствует authorization.

Огромный generated.ts

Решения:

  • near-operation-file;
  • modular architecture;
  • code splitting.

Конфликт имен

GraphQL:

type Query

может конфликтовать с локальными типами.

Решение:

config:
  typesPrefix: Gql

Пример полного generated endpoint

export const injectedRtkApi =
    api.injectEndpoints({
        endpoints: (build) => ({
            GetUser: build.query<
                GetUserQuery,
                GetUserQueryVariables
            >({
                query: (variables) => ({
                    document: GetUserDocument,
                    variables,
                }),
            }),

            CreateUser: build.mutation<
                CreateUserMutation,
                CreateUserMutationVariables
            >({
                query: (variables) => ({
                    document: CreateUserDocument,
                    variables,
                }),
            }),
        }),
    })

Преимущества генерации из GraphQL схемы

Снижение количества ручного кода

Без генерации требуется вручную:

  • писать endpoints;
  • создавать hooks;
  • описывать типы;
  • поддерживать синхронизацию со схемой.

Code Generator автоматизирует эти задачи.


Строгая типизация

Любое изменение схемы немедленно отражается в типах.

Ошибки обнаруживаются:

  • на этапе генерации;
  • во время typecheck;
  • внутри IDE.

Единый источник истины

GraphQL schema становится центральным контрактом между frontend и backend.


Улучшение DX

Автоматически появляются:

  • autocomplete;
  • type inference;
  • проверка аргументов;
  • проверка полей;
  • безопасный refactoring.

Ограничения подхода

Большие generated файлы

Крупные схемы могут генерировать десятки тысяч строк.


Сложность настройки

Интеграция требует:

  • GraphQL knowledge;
  • понимания codegen;
  • настройки pipeline.

Зависимость от схемы

Любые breaking changes backend могут ломать frontend генерацию.


Практика организации production-проекта

Распространенная структура:

src/
├── shared/api/
│   ├── baseApi.ts
│   ├── graphql/
│   │   ├── user/
│   │   ├── post/
│   │   └── comment/
│   └── generated/

Рекомендуемые настройки

config:
  exportHooks: true
  exposeDocument: true
  exposeQueryKeys: true
  addTagTypes: true
  avoidOptionals: true
  immutableTypes: true
  maybeValue: T | null

immutableTypes

Генерация readonly типов:

readonly id: string

Это уменьшает риск случайных мутаций.


avoidOptionals

Вместо:

name?: string

генерируется:

name: string | null

Такой подход лучше соответствует GraphQL semantics.


maybeValue

Настройка nullable типов:

maybeValue: T | null

или:

maybeValue: T | undefined

Полная схема процесса

GraphQL Backend
       ↓
Schema
       ↓
GraphQL Code Generator
       ↓
RTK Query endpoints
       ↓
Generated hooks
       ↓
React components
       ↓
Automatic cache management
       ↓
Type-safe frontend