В первой части мы настроили вполне себе современный REST API – использовали nodejs
, mongodb
и бэкбон hapi
, пришло время сделать наш API сервер еще более современным. Наверное, вы и читать эту статью начали только ради GraphQL
. Если с GraphQL все более-менее понятно, то последней технологией станет авто документирование нашего API средствами Swagger
. Но обо все по-порядку.
GraphQL
Собственно, почему GraphQL? Некоторые наши коллеги представляют что GraphQL пришел на смену REST API, как тот пришел в свое время чтобы сменить SOAP. В общем-то, нет никаких особых причин в этом утверждении и оба архитектурных стиля заслуживают на существование в 2018-м году.
GraphQL как минимум способен решить две болевые точки REST API:
- избыточность данных – не всегда нам нужны все поля, которые возвращает метод API;
- недостаточность данных – не всегда нам достаточно данных, которые возвращает один метод API и приходится вызывать еще один.
Вводные данные вы без труда найдете в сети, так что предлагаю вернуться к нашему API серверу.
Чтобы добавить GraphQL, нужно установить несколько дополнительных пакетов:
yarn add graphql apollo-server-hapi
graphql
– основной пакет для работы с новой архитектурой, а apollo-server-hapi
обеспечивает связь с нашим бекбоном.
Теперь в корне нашего проекта сделаем новую папку graphql
, а в ней файл PaintingType.js
следующего содержания:
const graphql = require('graphql'); const { GraphQLObjectType, GraphQLString } = graphql; const PaintingType = new GraphQLObjectType({ name: 'Painting', fields: () => ({ id: { type: GraphQLString }, name: { type: GraphQLString }, url: { type: GraphQLString }, techniques: { type: GraphQLString } }) }); module.exports = PaintingType;
Сначала мы подключили модуль graphql, потом декомпозировали объект graphql. Строка 3 у нас аналогична такой записи:
const GraphQLObjectType = graphql.GraphQLObjectType; const GraphQLString = graphql.GraphQLString;
Далее мы создали новый объект GraphQLObjectType, Для всех элементов объекта в GraphQL должен быть определен тип, т.к. GraphQL – язык со статической типизацией. Сейчас все наши элементы имеют тип GraphQLString
.
Это был наш главный запрос для обработки запросов о Painting. Теперь нужно встроить его в наш сервер. Создаем еще один файлик в папке graphql
с названием schema.js
const graphql = require('graphql'); const PaintingType = require('./PaintingType'); const { GraphQLObjectType, GraphQLString, GraphQLSchema } = graphql; const RootQuery = new GraphQLObjectType({ name: 'RootQueryType', fields: { painting: { type: PaintingType, args: { id: { type: GraphQLString } }, resolve(parent, args){ // дополнительная логика } } } }); module.exports = new GraphQLSchema({ query: RootQuery });
Обратите внимание, что теперь наш код запроса чуток более запутан, чем без GraphQL. У нас теперь painting – это поле, у которого тип из нашей схемы PaintingType, а его id – агрумент, т.к. именно по этому полю мы будем делать выборки из Монги.
Ну и в конце у нас есть функция resolve(), которая обрабатывает все наши данные. Давайте для примера посмотрим на формат запроса GraphQL:
{ painting(id: "5b3fc02f3add950b79393ea6") { name, url } }
А в самом конце у нас Компонент экспортирует данные.
Вернемся в index.js и подключим всю эту красоту. Нам нужно зарегистрировать при старте сервера модули для работы с GraphQL, давайте сделаем это:
await server.register({ plugin: graphiqlHapi, options: { path: '/graphiql', graphiqlOptions: { endpointURL: '/graphql' }, route: { cors: true } } }); await server.register({ plugin: graphqlHapi, options: { path: '/graphql', graphqlOptions: { schema }, route: { cors: true } } });
Обратите внимание, что модулей у нас два и graphiql это не graphql. Он представляет собой браузерную IDE, которая дает возможности проверять свои graphql query и какой нам возвращается ответ.
Сохраняем это все и давайте откроем адрес: http://localhost:4000/graphiql
Все конечно круто, но почему мы до сих пор получаем пустые ответы? Варианта тут два:
- у нас нет painting с id = 2;
- а даже если у нас есть и такой id, то мы все равно еще не получаем данные из самой Монги. Функция resolve() у нас все еще пустая, давайте это исправим.
В schema.js добавим следующее:
const Painting = require('./../models/Painting');
И в функцию resolve
return Painting.findById(args.id)
Все вместе должно получиться так:
const graphql = require('graphql'); const PaintingType = require('./PaintingType'); const Painting = require('./../models/Painting'); const { GraphQLObjectType, GraphQLString, GraphQLSchema } = graphql; const RootQuery = new GraphQLObjectType({ name: 'RootQueryType', fields: { painting: { type: PaintingType, args: { id: { type: GraphQLString } }, resolve(parent, args){ return Painting.findById(args.id) } } } }); module.exports = new GraphQLSchema({ query: RootQuery });
Снова пробуем graphiql:
Все почти круто, у нас тут видите ли немного неисправна модель, давайте исправим это. Собственно нужно просто изменить тип со [String] на String.
const PaintingSchema = new Schema({ name: String, url: String, technique: String });
Теперь все работает! Можно добавить еще пару объектов через Postman и поиграться с выводом необходимых полей.
Swagger
От крутого современного API нас отделяет только автодокументация средствами Swagger. Давайте добавим ее. Установка очень простая, но результат отличный – вы практически избавляетесь от необходимости создавать какие-то документации вне кода, забудьте про приватные сайты, вердовские документы и прочее.
Для начала установим пакеты:
yarn add hapi-swagger inert vision
Подключим плагины, воспользовавшись документацией с Гитхаба для пакета happi-swagger:
const swaggerOptions = { info: { title: 'Documentation about API for Painting ', version: Pack.version, }, }; await server.register([ Inert, Vision, { plugin: HapiSwagger, options: swaggerOptions } ]);
Сохраняем, заходим на http://localhost:4000/documentation
Все работает, но осталось добавить документацию к нашим endpoints. В секции server.route() добавим пару полей:
{ method: 'GET', path: '/api/v1/paintings', config: { description: 'Get Painting', tags: ['api', 'v1', 'someone-tag'] }, handler: (req, reply) => { return Painting.find(); } }, { method: 'POST', path: '/api/v1/paintings', config: { description: 'Add new Painting', tags: ['api', 'painting'] }, handler: (req, reply) => { const { name, url, techniques } = req.payload; const painting = new Painting({ name, url, techniques }); return painting.save(); } }
Сохраняем, проверяем:
Вот теперь все круто. Всем отличного кодинга.
Было бы здорово, если бы вы оставили ссылочку на оригинальную серию статей
Готово! Спасибо, что обратили внимание :)