API на Nodejs, GraphQL, MongoDB, Hapi и Swagger ч. 2

Работа сис. админа
2 мин. на чтение

В первой части мы настроили вполне себе современный 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();
			}
		}

Сохраняем, проверяем:

Вот теперь все круто. Всем отличного кодинга.

Оригинал статьи на English.

Ihor Chyshkala

Пишу статьи про ИТ в свободное от работы время.

Оцените автора
Авторский блог Игоря Чишкалы

Этот сайт использует Akismet для борьбы со спамом. Узнайте, как обрабатываются ваши данные комментариев.

  1. Виктор

    Было бы здорово, если бы вы оставили ссылочку на оригинальную серию статей

    Ответить
    1. Ihor Chyshkala автор

      Готово! Спасибо, что обратили внимание :)

      Ответить