GraphQL Resolvers (Резолверы)
Что такое Resolver?
Resolver — это функция, которая возвращает данные для поля в GraphQL схеме. Это "реализация" того, как получить данные.
Schema (ЧТО) → Resolver (КАК)Синтаксис базового резолвера
typescript
const resolvers = {
Query: {
user: (parent, args, context, info) => {
return {
id: '1',
name: 'John Doe'
};
}
}
};Параметры резолвера
typescript
function resolver(parent, args, context, info) {
// parent - результат резолвера родительского поля
// args - аргументы переданные в запросе
// context - объект с общей информацией (token, БД и т.д)
// info - информация о запросе
}Пример с параметрами
typescript
const resolvers = {
Query: {
user: (parent, args, context, info) => {
// parent = undefined (это Query - корневой тип)
// args = { id: '123' }
// context = { db, userId, token }
// info = { fieldName: 'user', ... }
return context.db.users.find(u => u.id === args.id);
}
}
};Резолверы для вложенных полей
typescript
const typeDefs = gql`
type User {
id: ID!
name: String!
posts: [Post!]!
}
type Post {
id: ID!
title: String!
author: User!
}
`;
const resolvers = {
Query: {
user: (_, { id }, { db }) => {
return db.users.find(u => u.id === id);
}
},
User: {
posts: (parent, args, { db }) => {
// parent - это объект User
return db.posts.filter(p => p.authorId === parent.id);
}
},
Post: {
author: (parent, args, { db }) => {
// parent - это объект Post
return db.users.find(u => u.id === parent.authorId);
}
}
};Асинхронные резолверы
typescript
const resolvers = {
Query: {
user: async (_, { id }, { db }) => {
return await db.users.findById(id);
}
},
User: {
posts: async (parent, _, { db }) => {
return await db.posts.find({ authorId: parent.id });
}
}
};Обработка ошибок
typescript
const resolvers = {
Mutation: {
createUser: async (_, { input }, { db }) => {
try {
// Валидация
if (!input.email.includes('@')) {
throw new Error('Invalid email');
}
// Создание
const user = await db.users.create(input);
return user;
} catch (error) {
throw new Error(`Failed to create user: ${error.message}`);
}
}
}
};Context в резолверах
typescript
// Создание сервера с context
const server = new ApolloServer({
typeDefs,
resolvers,
context: ({ req }) => {
// Получаем token из headers
const token = req.headers.authorization?.split('Bearer ')[1];
return {
db: database,
userId: getUserIdFromToken(token),
token
};
}
});Использование context:
typescript
const resolvers = {
Mutation: {
createPost: async (_, { input }, context) => {
if (!context.userId) {
throw new Error('Not authenticated');
}
return context.db.posts.create({
...input,
authorId: context.userId
});
}
}
};Default резолверы
GraphQL автоматически создает резолверы для полей:
typescript
// Если у вас нет явного резолвера для User.name,
// GraphQL автоматически вернет parent.name
const user = { id: '1', name: 'John' };
// GraphQL сделает: (parent) => parent.name === 'John'Резолверы с трансформацией
typescript
const resolvers = {
User: {
fullName: (parent) => {
return `${parent.firstName} ${parent.lastName}`;
},
age: (parent) => {
const birthDate = new Date(parent.birthDate);
return Math.floor((Date.now() - birthDate) / 31536000000);
}
}
};Batch резолверы
Проблема: N+1
typescript
// Плохо - запрос для каждого поста
const resolvers = {
User: {
posts: (parent, _, { db }) => {
// Для каждого пользователя - отдельный запрос!
return db.posts.find({ authorId: parent.id });
}
}
};Решение: DataLoader
typescript
import DataLoader from 'dataloader';
const postsByUserIdLoader = new DataLoader(async (userIds) => {
const posts = await db.posts.find({
authorId: { $in: userIds }
});
return userIds.map(userId =>
posts.filter(p => p.authorId === userId)
);
});
const resolvers = {
User: {
posts: (parent, _, context) => {
return context.postsByUserIdLoader.load(parent.id);
}
}
};Middleware резолверы
typescript
// Middleware для логирования
const loggingMiddleware = (fn) => async (...args) => {
console.log(`Resolver called with args:`, args[1]);
const result = await fn(...args);
console.log(`Resolver returned:`, result);
return result;
};
const resolvers = {
Query: {
user: loggingMiddleware(async (_, { id }, { db }) => {
return await db.users.findById(id);
})
}
};Резолверы с проверкой доступа
typescript
const requireAuth = (fn) => async (...args) => {
const context = args[2];
if (!context.userId) {
throw new Error('Not authenticated');
}
return fn(...args);
};
const requireRole = (role) => (fn) => async (...args) => {
const context = args[2];
const user = await context.db.users.findById(context.userId);
if (user.role !== role) {
throw new Error(`Required role: ${role}`);
}
return fn(...args);
};
const resolvers = {
Mutation: {
deleteUser: requireAuth(
requireRole('ADMIN')(async (_, { id }, { db }) => {
return await db.users.deleteById(id);
})
)
}
};Лучшие практики
✅ Делайте:
- Используйте context для БД и authentication
- Обрабатывайте ошибки с понятными сообщениями
- Используйте DataLoader для батчинга
- Кешируйте результаты где возможно
- Логируйте важные операции
❌ Не делайте:
- ❌ Не блокируйте операции
- ❌ Не делайте N+1 запросы
- ❌ Не забывайте проверки доступа
- ❌ Не доверяйте пользовательским данным
Дальше
Изучите Apollo Server для создания полноценного сервера.