Создаем мощный 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