Проектирование базы кода API GraphQL в Node.js

Большинство руководств по GraphQL не научат, как разделять схемы и распознаватели аналогично тому, как вы модулируете маршруты и контроллеры в типичном REST API, а также как задавать и загружать определения типов с помощью файлов .grapqhl вместо шаблонных литералов. В этой статье мы познакомимся с этими важными принципами.

Чтобы это руководство оказалось полезным, вам нужно базово ознакомиться с GraphQL и принципами его работы. Помимо этого, можно заглянуть в репозиторий.

Начальная настройка

Начнем с базового сервера и установим следующие зависимости:

npm i express express-graphql graphql @graphql-tools/schema
  • express  —  фреймворк для NodeJS.
  • express-graphql  —  модуль, который позволяет создавать API GraphQL с помощью Express.
  • graphql  —  эталонная реализация JavaScript для GraphQL.
  • @graphql-tools/*  —  набор пакетов, упрощающих работу с GraphQL.
const express = require('express');
const { graphqlHTTP } = require('express-graphql');
const { makeExecutableSchema } = require('@graphql-tools/schema');

const server = express();
const PORT = process.env.PORT || 5000;

const schema = makeExecutableSchema({
typeDefs: `type Query {
_empty: String
}`,
resolvers: {},
});

server.use(
'/graphql',
graphqlHTTP({
schema,
graphiql: true,
})
);

server.listen(PORT, () => console.log(`Listening on PORT ${PORT}`));

Обратите внимание: здесь используется makeExecutableSchema из @graphql-tools, а не buildSchema из graphql, потому что последняя ограничивает функциональность схемы. Подробнее об этом  —  в ответе на StackOverflow.

Данные для работы 

Для работы с API понадобятся данные. Здесь мы будем использовать фиктивные (хранятся в JS-файле). При этом концепции останутся неизменными.

Создайте каталог с именем data в корневом каталоге базы кода и разместите в нем следующее:

const authors = [
  { id: 1, name: 'J. K. Rowling' },
  // ... еще авторы
];

const books = [
  { id: 6, name: 'Forrest Gump', authorId: 2 },
  { id: 7, name: 'The Way of Shadows', authorId: 3 },
  { id: 8, name: 'Beyond the Shadows', authorId: 3 },
  // ... еще книги
];

exports.authors = authors;
exports.books = books;

Или же просто скопируйте и вставьте данные из этого источника. Есть две сущности: автор и книга. У автора есть набор книг, а у книги есть автор.

Определяем модульную схему

Создадим два новых каталога в корневом каталоге приложения:

  • схемы (для хранения определений типов для авторов и книг);
  • распознаватели (для хранения функций распознавания авторов и книг).
root
└─ resolvers
├─ authors.js
├─ authors.js
└─ index.js
└─ schemas
├─ authors.graphql
└─ books.graphql
└─ index.graphql
├─ package.json
└─ server.js

index.js в распознавателях и index.graphql в каталоге схем  —  точки встречи всех распознавателей и схем соответственно.

// index.graphqltype Query {
  _empty: String
  # здесь определите любые корневые запросы
}

type Mutation {
  _empty: String
  # здесь определите любые корневые запросы
}

Сейчас мы зададим только пустой запрос типа Query, потому что собираемся определить запросы для книг и авторов позже в соответствующих им файлах схемы.

Обратите внимание: текущая версия GraphQL не позволяет использовать пустой тип, даже если вы собираетесь позже его расширить. Поэтому здесь добавлено пустое поле.

// authors.graphql

type Author {
  id: Int!
  name: String!
  books: [Book]
}

# расширение корневого типа запроса, который мы определили в index.grapqhl
extend type Query {
  authors: [Author]
  author(id: Int!): Author!
}

Аналогично, схема для книг выглядит следующим образом:

// books.graphql

type Book {
  id: Int!
  title: String!
  authorId: Int!
  author: Author!
}

extend type Query {
  books: [Book]!
  book(id: Int!): Book!
}

input NewBook {
  title: String!
  authorId: Int!
}

extend type Mutation {
  createBook(newBook: NewBook): Book!
}

Для определения схемы и типов применяется SDL GraphQL. Вы также можете сгенерировать схему программным образом. Поскольку GraphQL можно совмещать с любым внутренним языком программирования, то лучше использовать SDL.

Если вы работаете в VSCode, и у вас не отображается подсветка синтаксиса/автозаполнение в файлах .graphql, то установите официальное расширение GraphQL.

Определение распознавателей

Мы определили схему для книг и авторов. Пришло время создавать распознаватели для авторов и книг.

// resolvers/authors.js

const { authors, books } = require('../data');

const authorsResolvers = {
  Query: {
    authors: () => authors,
    author: (parent, { id }) => {
      return authors.find((author) => author.id === id);
    }
  },
  Author: {
    books: (author) => {
      return books.filter((book) => book.authorId === author.id);
    }
  }
};

module.exports = authorsResolvers;

Распознаватель для книг:

// resolvers/books.js

const { authors, books } = require('../data');

const booksResolvers = {
  Query: {
    books: () => books,
    book: (parent, { id }) => {
      return books.find((book) => book.id === id);
    }
  },
  Book: {
    author: (book) => {
      return authors.find((author) => author.id === book.authorId);
    }
  },
  Mutation: {
    createBook: (parent, { newBook }) => {
      const createdBook = { id: books.length + 1, ...newBook };
      books.push(createdBook);
      return createdBook;
    }
  }
};

module.exports = booksResolvers;

В файле index.js из каталога распознавателей объединим распознаватели для книг и авторов в один объект.

// resolvers/index.js

const authorsResolvers = require('./authors');
const booksResolvers = require('./books');
const rootResolver = {};
const resolvers = [
  rootResolver,
  authorsResolvers,
  booksResolvers,
];

module.exports = resolvers;

Вот что мы здесь делаем:

  • импортируем все распознаватели (в данном случае книг и авторов);
  • определяем корневой распознаватель, экспортирующий массив распознавателей (graphql-tools автоматически позаботится об их объединении).

Внедрение схемы и распознавателей на сервер

Наконец, можно импортировать схемы и распознаватели в файл server.js. Поскольку для определения типов здесь используются файлы .graphql, для их чтения воспользуемся двумя небольшими graphql-инструментами:

npm i @graphql-tools/load @graphql-tools/graphql-file-loader

Аналогично в server.js:

const express = require('express');
const { graphqlHTTP } = require('express-graphql');
const { makeExecutableSchema } = require('@graphql-tools/schema');
const { loadSchemaSync } = require('@graphql-tools/load');
const { GraphQLFileLoader } = require('@graphql-tools/graphql-file-loader');
const graphqlResolver = require('./resolvers');

const server = express();
const PORT = process.env.PORT || 5000;

const schema = makeExecutableSchema({
typeDefs: loadSchemaSync('schemas/**/*.graphql', {
loaders: [new GraphQLFileLoader()],
}),
resolvers: graphqlResolver,
});

server.use(
'/graphql',
graphqlHTTP({
schema,
graphiql: true,
})
);

server.listen(PORT, () => console.log(`Listening on PORT ${PORT}`));

Для загрузки определений типов применяется шаблон glob, поэтому loadSchemaSync загрузит все файлы .graphql из каталога схем и объединит их в единую схему.

Мы получили функциональный сервер GraphQL, работающий на localhost:5000/graphql, и можем выполнять следующие запросы:

Аутентификация

Добавить аутентификацию в API GraphQL очень легко. Просто добавьте промежуточный обработчик перед определением маршрута /graphql в server.js

const express = require(’express’);
const { graphqlHTTP } = require(’express-graphql’);
const { makeExecutableSchema } = require(’@graphql-tools/schema’);
const { loadSchemaSync } = require(’@graphql-tools/load’);
const { GraphQLFileLoader } = require(’@graphql-tools/graphql-file-loader’);
const graphqlResolver = require(’./resolvers’);

const server = express();
const PORT = process.env.PORT || 5000;

const schema = makeExecutableSchema({
typeDefs: loadSchemaSync(’schemas/**/*.graphql’, {
loaders: [new GraphQLFileLoader()],
}),
resolvers: graphqlResolver,
});

server.use((req, res, next) => {
// извлечь токен из заголовков запроса
const token = req.header(’Authorization’);

// TODO: верифицировать токен

// TODO: привязать пользователя

// позже мы можем обратиться к свойству isAuthenticated
// в функциях распознавания, чтобы проверить,
// был ли пользователь аутентифицирован
req.isAuthenticated = Boolean(token);

// вызовите следующее промежуточное программное обеспечение
// когда пользователь либо аутентифицирован, либо нет
next();
});

server.use(
'/graphql’,
graphqlHTTP({
schema,
graphiql: true,
})
);

server.listen(PORT, () => console.log(`Listening on PORT ${PORT}`));

А в функции распознавателя можно сделать следующее:

// resolvers/authors.js

const authorsResolvers = {
  Query: {
    authors: (parent, args, context) => {
      if (!context.isAuthenticated) {
        throw new Error('You are not authenticated');
      }      return authors;
    },
    author: (parent, { id }) => {
      return authors.find((author) => author.id === id);
    }
  },
};

Состав распознавателей

Поскольку нам необходимо выполнить аутентификацию для всех распознавателей, вместо проверки подлинности пользователя в каждом отдельном случае, можно воспользоваться инструментом от @graphql-tools. Он позволяет один раз определить промежуточное ПО аутентификации для набора запросов или мутаций:

npm i @graphql-tools/resolvers-composition

А также:

// resolvers/authors.js

const { composeResolvers } =
  require('@graphql-tools/resolvers-composition');
const { authors, books } = require('../data');const authorsResolvers = {
  Query: {
    authors: (parent, args, context) => {
      return authors;
    },
    author: (parent, { id }) => {
      return authors.find((author) => author.id === id);
    }
  },
};

const authenticateReq = (next) => {
  return (root, args, context, info) => {
    if (!context.isAuthenticated) {
      throw new Error('You are not authorized');
    }
    return next(root, args, context, info);
  };
};

// запустите промежуточное ПО аутентификации для всех типов и всех полей
// на распознавателе для авторов
module.exports = composeResolvers(authorsResolvers, {
  '*.*': [authenticateReq],
});

Теперь всякий раз, когда вы запрашиваете auhtors или author(id: id_here), промежуточное ПО authenticateReq запускается перед функцией распознавателя, гарантируя, что в функцию распознавателя поступают только аутентифицированные запросы.

Для ресурса книг в API можете следовать тому же подходу. Ищите поддерживаемые шаблоны glob для распознавателей здесь.


Это отправная точка любого сложного проекта GraphQL. Такая структура поможет легко поддерживать код, поскольку ресурсы являются модульными.

Спасибо за чтение! Репозиторий с кодом находится здесь.

Читайте также:

Читайте нас в TelegramVK и Яндекс.Дзен


Перевод статьи Haseeb Anwar: Architecting a GraphQL API Codebase in Node.js

Предыдущая статьяJava-библиотеки, которые повысят вашу производительность
Следующая статьяТенденции в области программного обеспечения в 2022 году: 22 прогноза