graphql
scalar Date
type User {
id: ID!
name: String!
email: String!
createdAt: Date!
}
type Query {
getUser(id: ID!): User
listUsers(limit: Int = 10): [User!]!
}
input CreateUserInput {
name: String!
email: String!
}
type Mutation {
createUser(input: CreateUserInput!): User!
}
// codegen.ts
import type { CodegenConfig } from '@graphql-codegen/cli';
const config: CodegenConfig = {
schema: './src/schema.graphql',
generates: {
'./src/generated/graphql.ts': {
plugins: [
'typescript',
'typescript-resolvers',
'typescript-document-nodes'
],
config: {
useIndexSignature: true,
contextType: '../context#Context',
mapperTypeSuffix: 'Model',
mappers: {
User: '../models#UserModel'
},
scalars: {
Date: 'Date'
}
}
}
}
};
export default config;
// src/models/user.ts
export interface UserModel {
id: string;
name: string;
email: string;
created_at: Date; // Разница в именовании между БД и API
}
// src/context.ts
import type { UserModel } from './models/user';
export interface Context {
userLoader: DataLoader<string, UserModel>;
// Другие инструменты контекста
}
// Пример типизированного резолвера
const userResolver: QueryResolvers<Context>['getUser'] = async (
_parent,
{ id },
context,
_info
) => {
/*
* Теперь резолвер знает:
* - args автоматически типизированы как { id: string }
* - context соответствует нашему интерфейсу Context
* - Возвращаемое значение должно соответствовать User | null | undefined
*/
return context.userLoader.load(id);
};
const createUserResolver: MutationResolvers<Context>['createUser'] = async (
_parent,
{ input },
context
) => {
// Система знает структуру input благодаря CreateUserInput
if (input.email.includes('spam')) {
throw new UserInputError('Invalid email detected');
}
const newUser = await userService.createUser({
name: input.name,
email: input.email
});
// Автоматическая проверка соответствия типа UserModel
return newUser;
};
При работе с контекстом укажите:
import { UserModel } from '../models/user';
import { DataLoader } from '../utils/data-loader';
type Loaders = {
userLoader: DataLoader<string, UserModel>;
};
export type Context = {
loaders: Loaders;
userId?: string;
};
// В Apollo Server
const server = new ApolloServer<Context>({
typeDefs,
resolvers,
context: ({ req }) => ({
userId: getUserIdFromRequest(req),
loaders: {
userLoader: createUserLoader()
}
})
});
Для оптимизации производительности используйте DataLoader:
type UserModel = {
userId: string;
name: string;
};
export const createUserLoader = () => {
return new DataLoader<string, UserModel>(async (userIds) => {
const users = await db.user.findMany({
where: { userId: { in: [...userIds] } },
});
return userIds.map((id) =>
users.find((u) => u.userId === id) || new NotFoundError(`User ${id}`)
);
});
};
Для прохождения полного цикла разработки:
npm start # Запускает сервер разработки с пересборкой при изменениях
npm run codegen # Регенерирует типы при изменении схемы
В заключение: чем вы выигрываете
-
Типизированные резолверы: Каждый резолвер знает точную форму своих параметров и ожидаемый тип результата. Ошибки несоответствия типов обнаруживаются при компиляции, а не в рантайме.
-
Согласованность контракта: Изменения в схеме немедленно отражаются в TypeScript типах, вынуждая разработчиков корректировать реализацию в соответствии с новыми требованиями.
-
Контекстная безопасность: Типизированный контекст исключает ошибки доступа к несуществующим свойствам и обеспечивает доступность инструментов там, где они реально нужны.
-
Совместимость с ORM: Благодаря гибкой системе маппингов вы поддерживаете связь между отличными моделями GraphQL и объектами предметной области.
Для команд, требовательных к качеству кода, сочетание GraphQL Code Generator и TypeScript – это не просто приятная опция, а необходимый инструмент для создания предсказуемых, легко поддерживаемых бэкенд-систем. При правильной настройке он окупает вложенные усилия многократным сокращением ошибок типа null-pointer exceptions или неожиданных undefined значений.