GraphQL.js для JavaScript (Node.js): Полное руководство
1. Что это и зачем нужна?
GraphQL.js — это эталонная реализация спецификации GraphQL на языке JavaScript. Она предоставляет полный набор инструментов для создания, выполнения и валидации GraphQL-запросов на серверной стороне (Node.js) и в браузере. В отличие от REST API, где каждый эндпоинт возвращает фиксированную структуру данных, GraphQL позволяет клиенту точно запросить только те данные, которые ему нужны, и ничего лишнего. Это решает проблему over-fetching (получение избыточных данных) и under-fetching (недостаток данных), что критически важно для производительности мобильных приложений и сложных веб-интерфейсов.
Библиотека GraphQL.js является фундаментом для всех популярных GraphQL-фреймворков в экосистеме JavaScript, таких как Apollo Server, Express-GraphQL, Yoga и других. Она берет на себя тяжелую работу по парсингу запросов, их валидации, выполнению резолверов и формированию ответа в строгом соответствии со спецификацией. Используя GraphQL.js напрямую, вы получаете максимальный контроль над процессом обработки запросов, что особенно важно при создании кастомных middleware, инструментов для тестирования или интеграции с нестандартными источниками данных.
Основные преимущества использования GraphQL.js включают: строгую типизацию схемы, автоматическую генерацию документации (через Introspection Query), возможность эффективно объединять данные из нескольких источников (базы данных, REST API, микросервисы) в одном запросе, а также встроенные механизмы для обработки ошибок и пагинации. Это делает библиотеку незаменимой для построения современных, масштабируемых и гибких API.
2. Установка
Установка библиотеки производится через менеджер пакетов npm (Node Package Manager). Убедитесь, что у вас установлен Node.js версии 12 или выше.
npm install graphql
Для использования в проекте с поддержкой ES-модулей (type: "module" в package.json) импортируйте функции следующим образом:
import { graphql, buildSchema } from 'graphql';
Для CommonJS (стандартный подход в Node.js):
const { graphql, buildSchema } = require('graphql');
3. Быстрый старт — минимальный рабочий пример
Ниже представлен простейший сервер, который принимает GraphQL-запрос и возвращает приветствие. Этот пример демонстрирует ядро библиотеки: определение схемы, резолверы и выполнение запроса.
const { graphql, buildSchema } = require('graphql');
// 1. Определяем схему (Schema Definition Language - SDL)
const schema = buildSchema(`
type Query {
hello: String
}
`);
// 2. Определяем корневой резолвер (root resolver)
const rootValue = {
hello: () => {
return 'Hello, GraphQL.js!';
},
};
// 3. Выполняем запрос
const query = '{ hello }';
graphql({
schema,
source: query,
rootValue,
}).then((response) => {
console.log(JSON.stringify(response, null, 2));
// Вывод: { "data": { "hello": "Hello, GraphQL.js!" } }
});
4. Основные методы/функции/классы
4.1. buildSchema(schemaString: string): GraphQLSchema
Что делает: Парсит строку, написанную на языке схем GraphQL (SDL), и создает объект GraphQLSchema. Это самый быстрый способ определить схему без использования программного конструктора.
const { buildSchema } = require('graphql');
const schema = buildSchema(`
type Query {
user(id: ID!): User
}
type User {
id: ID
name: String
}
`);
4.2. graphql(args: GraphQLArgs): Promise<GraphQLResult>
Что делает: Основная функция для выполнения GraphQL-запроса. Принимает объект с параметрами (схема, строка запроса, корневое значение, контекст, переменные) и возвращает Promise с результатом выполнения.
const { graphql, buildSchema } = require('graphql');
const schema = buildSchema(`type Query { echo(msg: String!): String }`);
const rootValue = { echo: ({ msg }) => `Echo: ${msg}` };
graphql({ schema, source: '{ echo(msg: "test") }', rootValue }).then(console.log);
// { data: { echo: 'Echo: test' } }
4.3. GraphQLSchema(config: GraphQLSchemaConfig): GraphQLSchema
Что делает: Класс-конструктор для программного создания схемы. Позволяет задавать типы, поля и резолверы через объекты конфигурации, а не через SDL. Дает больше контроля, например, для добавления кастомных скалярных типов.
const { GraphQLSchema, GraphQLObjectType, GraphQLString } = require('graphql');
const schema = new GraphQLSchema({
query: new GraphQLObjectType({
name: 'Query',
fields: {
status: {
type: GraphQLString,
resolve: () => 'OK',
},
},
}),
});
4.4. GraphQLObjectType(config: GraphQLObjectTypeConfig): GraphQLObjectType
Что делает: Определяет объектный тип в схеме. Содержит поля, каждое из которых имеет свой тип и резолвер. Используется для создания сложных типов данных.
const { GraphQLObjectType, GraphQLString, GraphQLInt } = require('graphql');
const UserType = new GraphQLObjectType({
name: 'User',
fields: {
id: { type: GraphQLInt },
name: { type: GraphQLString },
},
});
4.5. GraphQLNonNull(type: GraphQLNullableType): GraphQLNonNull
Что делает: Модификатор типа, который указывает, что значение поля не может быть null. Если резолвер вернет null для такого поля, GraphQL вызовет ошибку выполнения.
const { GraphQLNonNull, GraphQLString } = require('graphql');
const nonNullString = GraphQLNonNull(GraphQLString);
// Использование в схеме: id: { type: new GraphQLNonNull(GraphQLInt) }
4.6. GraphQLList(type: GraphQLType): GraphQLList
Что делает: Модификатор типа для обозначения списка (массива) элементов определенного типа. Позволяет возвращать коллекции данных.
const { GraphQLList, GraphQLString } = require('graphql');
const listOfStrings = GraphQLList(GraphQLString);
// Использование: users: { type: new GraphQLList(UserType) }
4.7. GraphQLEnumType(config: GraphQLEnumTypeConfig): GraphQLEnumType
Что делает: Определяет перечисляемый тип — набор фиксированных строковых значений. Полезен для статусов, ролей, категорий.
const { GraphQLEnumType } = require('graphql');
const RoleType = new GraphQLEnumType({
name: 'Role',
values: {
ADMIN: { value: 'admin' },
USER: { value: 'user' },
GUEST: { value: 'guest' },
},
});
4.8. GraphQLInputObjectType(config: GraphQLInputObjectTypeConfig): GraphQLInputObjectType
Что делает: Определяет входной объектный тип, используемый для передачи сложных аргументов в мутации или запросы. В отличие от GraphQLObjectType, не может иметь резолверов.
const { GraphQLInputObjectType, GraphQLString, GraphQLNonNull } = require('graphql');
const UserInputType = new GraphQLInputObjectType({
name: 'UserInput',
fields: {
name: { type: new GraphQLNonNull(GraphQLString) },
email: { type: GraphQLString },
},
});
4.9. execute(args: ExecutionArgs): PromiseOrValue<ExecutionResult>
Что делает: Низкоуровневая функция для выполнения запроса. В отличие от graphql, не включает в себя парсинг и валидацию — ожидает, что запрос уже представлен в виде DocumentNode. Используется для продвинутой оптимизации.
const { parse, validate, execute, buildSchema } = require('graphql');
const schema = buildSchema(`type Query { ping: String }`);
const document = parse('{ ping }');
const errors = validate(schema, document);
if (errors.length === 0) {
execute({ schema, document, rootValue: { ping: () => 'pong' } }).then(console.log);
}
4.10. parse(source: string | Source): DocumentNode
Что делает: Преобразует строку GraphQL-запроса в абстрактное синтаксическое дерево (AST). Это первый этап обработки запроса, который может быть выполнен отдельно для кэширования или анализа.
const { parse } = require('graphql');
const ast = parse('{ __typename }');
console.log(ast.definitions[0].kind); // 'OperationDefinition'
5. Реальный практический пример (100-200 строк кода)
Создадим простой API для управления списком задач (TODO) с поддержкой запросов, мутаций и аргументов. Этот пример показывает, как объединить несколько концепций библиотеки в работающий сервер.
const { graphql, buildSchema } = require('graphql');
// Имитация базы данных
let todos = [
{ id: 1, title: 'Изучить GraphQL.js', completed: false },
{ id: 2, title: 'Написать документацию', completed: true },
];
let nextId = 3;
// Определение схемы
const schema = buildSchema(`
type Todo {
id: ID!
title: String!
completed: Boolean!
}
type Query {
todos(completed: Boolean): [Todo]
todo(id: ID!): Todo
}
type Mutation {
addTodo(title: String!): Todo
toggleTodo(id: ID!): Todo
deleteTodo(id: ID!): Boolean
}
`);
// Корневой резолвер
const rootValue = {
todos: ({ completed }) => {
if (completed === undefined) return todos;
return todos.filter(todo => todo.completed === completed);
},
todo: ({ id }) => todos.find(todo => todo.id === Number(id)),
addTodo: ({ title }) => {
const newTodo = { id: nextId++, title, completed: false };
todos.push(newTodo);
return newTodo;
},
toggleTodo: ({ id }) => {
const todo = todos.find(todo => todo.id === Number(id));
if (!todo) throw new Error(`Todo with id ${id} not found`);
todo.completed = !todo.completed;
return todo;
},
deleteTodo: ({ id }) => {
const index = todos.findIndex(todo => todo.id === Number(id));
if (index === -1) return false;
todos.splice(index, 1);
return true;
},
};
// Выполнение тестовых запросов
async function main() {
// Запрос всех задач
const query1 = '{ todos { id title completed } }';
const res1 = await graphql({ schema, source: query1, rootValue });
console.log('All todos:', JSON.stringify(res1.data));
// Добавление новой задачи
const mutation1 = 'mutation { addTodo(title: "Купить молоко") { id title completed } }';
const res2 = await graphql({ schema, source: mutation1, rootValue });
console.log('Added todo:', JSON.stringify(res2.data));
// Запрос только незавершенных задач
const query2 = '{ todos(completed: false) { title } }';
const res3 = await graphql({ schema, source: query2, rootValue });
console.log('Incomplete todos:', JSON.stringify(res3.data));
// Удаление задачи
const mutation2 = 'mutation { deleteTodo(id: 1) }';
const res4 = await graphql({ schema, source: mutation2, rootValue });
console.log('Deleted todo with id 1:', res4.data);
}
main().catch(console.error);
6. Советы по производительности и типичные ошибки
Советы по производительности
- Используйте DataLoader для N+1 проблемы: Когда резолверы для списка объектов делают отдельные запросы к БД для каждого элемента, это приводит к N+1 проблеме. DataLoader (отдельная библиотека) позволяет батчить и кэшировать запросы на уровне одного HTTP-запроса.
- Кэшируйте парсинг и валидацию: Если вы получаете много одинаковых запросов (например,
{ __typename }), используйтеparseиvalidateотдельно и кэшируйте результат по строке запроса. Функцияgraphqlделает это каждый раз заново. - Ограничивайте глубину и сложность запросов: Злоумышленник может отправить глубоко вложенный запрос (например,
{ user { friends { user { friends {...} } } } }), который перегрузит сервер. Используйте библиотеки типаgraphql-depth-limitилиgraphql-query-complexityдля защиты. - Избегайте дорогих вычислений в резолверах: Резолверы должны быть легковесными. Если нужно выполнить тяжелую работу, делайте это асинхронно и возвращайте Promise. Рассмотрите использование "lazy loading" для полей, которые запрашиваются редко.
- Используйте
executeдля production: В высоконагруженных системах используйтеexecuteвместоgraphql, чтобы контролировать этапы парсинга и валидации, а также кэшировать AST.
Типичные ошибки
- Забыли вернуть Promise из асинхронного резолвера: Если резолвер помечен как
async, он всегда возвращает Promise. Если вы забыли