Создаем мощный API на Node.js, GraphQL, MongoDB, Hapi и Swagger

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

Мы будем создавать очень гибкий и мощный API на базе Node.js и GraphQL с документацией, созданной на Swagger.

Основой API будет Hapi.js – малоизвестный фреймворк, позволяющий создавать логику приложения, не отвлекаясь на архитектуру, а также получить больший контроль над процессом, нежели может предложить, например, Restify или Express. Рекомендуем посмотреть видеоурок по этому фреймворку для приобретения дополнительных знаний и получения общего представления о системе:

https://www.youtube.com/watch?&v=2lprC0yYeFw

Наш проект предусматривает интеграцию с клиентской частью на React, Vue или Angular.

Нам понадобится:

  • Установленный Node.js и MongoDB.
  • Базовые знания по JavaScript (если их недостаточно, вот вам прекрасный курс для саморазвития).
  • Терминал и текстовый редактор.

Начнем

Откройте терминал и создайте проект. Внутри каталога проекта мы инициализируем Node проект.

Далее нам нужно установить сервер Hapi вместе с зависимостями. Для этого можно использовать как Yarn, так и NPM.

yarn add hapi nodemon

Вторая зависимость – это nodemon. Он будет автоматически перезагружать наш сервер, когда будут внесены какие-либо изменения.

Настройка Hapi очень проста – создайте в корневой директории файлик index.js и наполните его следующим образом:

const hapi = require('hapi');

const server = hapi.server({
    port: 4000,
    host: 'localhost'
});


const init = async () => {
    await server.start();
    console.log(`Server running at: ${server.info.uri}`);
};

init();
  • Первое, что мы делаем – подключаем зависимости от hapi.
  • Во-вторых, добавляем константу server, которая создает новый экземпляр нашего сервера Hapi. В качестве аргументов мы передаем объект с параметрами port и host.
  • В-третьих, мы создаем метод init, внутри которого, у нас есть еще один асинхронный метод (server.start()), запускающий сервер.

Если у вас возникают трудности с async await, рекомендуем посмотреть видео на эту тему:

https://www.youtube.com/watch?v=568g8hxJJp4

Посмотрим, что получилось:

node index.js

Server running at: http://localhost:4000

Если перейти по этой ссылке

http://localhost:4000

мы должны увидеть следующее:

Результат очевиден, т. к. не хватает обработчика и роутов, которые мы добавим ниже. А пока научим nodemon запускать сервер. Для этого добавим три строчки кода в файлик package.json:

"scripts": {
    "start": "nodemon index.js"
  },

Теперь запускать стало намного удобнее:

Маршрутизация

Маршрутизация интуитивно понятна при использовании Hapi. Есть три основных управляющих компонента:

  • path – путь.
  • method – GET , POST или что-то другое.
  • handler – обработчик.
const hapi = require('hapi');

const server = hapi.server({
	port: 4000,
	host: 'localhost'
});

const init = async () => {
   server.route([
		{
		   method: 'GET',
		   path: '/',
		   handler: function(request, reply) {
		 	return '<h1>Our new API</h1>';
		   }
		});
   await server.start();
   console.log(`Server running at: ${server.info.uri}`);
};
init();

Внутри метода init мы добавили к серверу новый метод route с параметрами, переданными в качестве нашего аргумента.

Настройка базы данных

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

yarn add mogoose

+ строкой в файле:

const mongoose = require('mongoose');

Чтобы не запускать Mongo на локальной машине, мы будем использовать облачное хранилище mlab. Выбор пал на это облако из-за возможности бесплатного использования и простоты настройки (вы можете выбрать другой сервис). Создаем базу и пользователя – это и вся настройка mlab.

Подключаем mongoose к mlab

Чтобы связать мощный API с mongoose и mlab, нужно в файл index.js добавить несколько строк с параметрами подключения:

mongoose.connect('mongodb://indrek:test@ds231090.mlab.com:31090/powerful-api');

mongoose.connection.once('open', () => {
	console.log('connected to database');
});

Тут мы указали mongoose, к какой базе будем подключаться. Убедитесь, что вы используете правильные учетные данные. Можно попробовать запустить наш скрипт run, и если вы все правильно сделали, то в терминале появится сообщение: connected to database.

Хотите освежить свои знания по Mongo? Вот вам небольшой туториал:

https://www.youtube.com/watch?list=PL4cUxeGkcC9jpvoYriLI0bY8DOgWZfi6u&v=9OPP_1eAENg

Создание моделей

При работе с MongoDB нужно следовать правилам моделирования данных.

Мы просто объявляем свою схему для коллекций. Если говорить образно, то представляйте коллекции, как таблицы в базе данных SQL.

Давайте создадим директорию models, а внутри нее файл painting.js. Он будет хранить в себе все данные, относящиеся к картинам. Выглядеть такой файл будет следующим образом:

const mongoose = require('mongoose');
const Schema = mongoose.Schema;

/*
  notice there is no ID. That's because Mongoose will assign
  an ID by default to all schemas
*/

const PaintingSchema = new Schema({
	name: String,
	url: String,
	technique: [String]
});

module.exports = mongoose.model('Painting', PaintingSchema);
  • Мы подключаем в наш мощный API зависимости mongoose.
  • Объявляем нашу PaintingSchema, вызывая конструктор схемы mongoose и передавая параметры. Обратите внимание, как все жестко типизировано: поле name может содержать только строку, а technique – массив строк.
  • Экспортируем модель и называем ее Painting.

Попробуем извлечь все картины из базы. Для этого сначала нужно добавить строку в файл index.js:

const Painting = require('./models/Painting');

Чтобы каждая картина имела свой id (api/v1/paintings/{id}), добавим методы GET и POST в секцию с маршрутами. GET будет получать все картины, а POST – добавлять новые.

const init = async () => {

   server.route([
	{
	   method: 'GET',
	   path: '/',
	   handler: function(request, reply) {
	      return '<h1>Our new API</h1>';
	   }
	},
        {
	   method: 'GET',
	   path: '/api/v1/paintings',
	     handler: (req, reply) => {
	  	return Painting.find();
	     }
	},
	{
	   method: 'POST',
	   path: '/api/v1/paintings',
	   handler: (req, reply) => {
		const { name, url, technique } = req.payload;
		const painting = new Painting({
			name,
			url,
			technique
		});
	        return painting.save();
	}
   }
]);
  • Мы создали GET с путем /api/v1/paintings. Внутри обработчика мы вызываем схему mongoose. У него есть удобный метод – find(), который возвращает все картины, если ему ничего не передано в параметрах.
  • POST мы создали для того же пути. Остановимся подробнее на этом. Ранее в схеме мы определяли поля: name, url, techniques. Здесь мы просто принимаем аргументы из запроса и передаем их в схему mongoose. Потом вызывается метод save() и происходит сохранение.

Если перейти по ссылке

http://localhost:4000/api/v1/paintings

мы должны увидеть следующее:

Наш мощный API вернул пустой результат, т. к. в базе пока нет картин, но мы это исправим.

Для создания коллекции с запросами к API мы будем использовать очень крутой инструмент – Postman. Устанавливаем его по инструкции, и после этого заходим на веб-морду этого сервиса:

  • Слева вы можете видеть опции метода. Выберите там POST.
  • Следующее поле – URL. Это адрес, на который будет отправлен наш метод.
  • Синяя кнопка справа отправляет запрос.
  • Под URL поле с опциями. Кликните по body и заполните поля, как в примере ниже.
{
  "name": "Mona Lisa",
  "url": "https://en.wikipedia.org/wiki/Mona_Lisa#/media/File:Mona_Lisa,
               _by_Leonardo_da_Vinci,_from_C2RMF_retouched.jpg",
  "techniques": ["Portrait"]
}

Теперь еще раз откроем нашу ссылку:

http://localhost:4000/api/v1/paintings

Беремся за GraphQL

GraphQL помогает справиться с многими неприятными моментами, с которыми могут столкнуться традиционные API REST. Вот некоторые из них:

  • Over-fetching – в ответе есть данные, которые не используются.
  • Under-fetching  – получено недостаточно данных после выборки, что приводит к повторному запросу.

GraphQL стал настолько популярным отчасти потому, что у людей есть веские основания полагать, что он полностью заменит REST-так же, как REST заменил SOAP.

Устанавливаем:

yarn add graphql apollo-server-hapi

Apollo – это связующее звено между нашим Hapi-сервером и GraphQL.

Далее создадим новую папку с названием 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 },
        technique: { type: GraphQLString }
    })
});

module.exports = PaintingType;

Теперь нам нужно создать файл schema.js в этой же папке. Наполним его следующим образом:

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
});

В конце мы экспортируем наш корневой запрос и передаем его на сервер hapi. Обратите внимание на тип GraphQLSchema – это определение корневого запроса/схемы, которое мы передаем серверу.

Следующим шагом подключим к проекту GraphQL через файл index.js двумя строчками:

const { graphqlHapi, graphiqlHapi } = require('apollo-server-hapi');
const schema = require('./graphql/schema');

Зарегистрируем плагин:

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
	}
     }
});

Последний штрих

Последний шаг – наделим наш мощный API созданием документации. Для этого будем использовать мощный инструмент Swagger, позволяющий в полной мере воспользоваться спецификацией OpenAPI.

Устанавливаем:

yarn add hapi-swagger inert vision

Регистрируем:

await server.register([
	Inert,
	Vision,
		{
		  plugin: HapiSwagger,
		    options: {
			info: {
	   		   title: 'Paintings API Documentation',
			   version: Pack.version
			}
		    }
	        }
]);

Страничка документации доступна по уже известному адресу:

http://localhost:4000/documentation

Оригинал

Другие материалы по теме:

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ

eFusion
08 января 2020

11 типов современных баз данных: краткие описания, схемы и примеры БД

Любые данные где-то хранятся. Будь это интернет вещей или пароли в *nix. По...
admin
23 февраля 2017

SQL за 20 минут

Предлагаем вашему вниманию статью с кричащим названием "SQL за 20 минут". К...