Можно разработать просто лучший REST API, но без документации им просто никто не сможет пользоваться. Swagger - популярный фреймворк с развитой экосистемой инструментов, который помогает проектировать, писать, документировать и работать с RESTful API. Для Node.js есть модуль Swagger Node. С его помощью можно спроектировать API, используя mock-объекты, а только потом реализовать логику, написав соответствующие контроллеры. В данной статье мы воспользуемся подобной тактикой и попробуем Swagger в действии.
Установка и запуск примера
Для начала глобально установим swagger:
npm install -g swagger
Теперь создадим новый проект. Пускай это будет коллекция книг:
swagger project create books
В диалоге выбираем сервер express. Swagger сгенерирует нам следующую структуру:
-- api
---- controllers
------ hello_world.js
---- helpers
---- mocks
---- swagger
------ swagger.yaml
-- config
---- default.yaml
-- test
---- api
------ controllers
-------- hello_world.js
------ helpers
-- app.js
-- package.json
app.js
- это главный файл, который запускает наш сервер.
В директории api
находится все, что касается нашего API: контроллеры, вспомогательные модули, mock-объекты. Здесь следует обратить внимание на файл api/swagger/swagger.yaml
. В этом файле собственно и находится все описание API в формате YAML.
В файле config/default.yaml
находится описание конфигурации swagger.
В каталоге test
располагаются тесты для нашего кода.
При создании нового проекта swagger сгенерировал пример обработки GET заброса по маршруту /hello
. Поэтому мы можем видеть контроллер hello_world.js
и тест для него. Давайте попробуем запустить этот пример в редакторе Swagger Editor.
Запускаем сервер:
swagger project start
Сервер следит за файлами и при изменении перезапускается автоматически.
Теперь в другой командной строке запускаем редактор:
swagger project edit
Должно открыться окно браузера и там вы должны увидеть такую картину:
Слева отображается собственно наш файл swagger.yaml
с возможностью его редактирования. А вот справа находится самое интересное - красивый список маршрутов и методов. Их можно сворачивать/разворачивать и при этом сворачивается/разворачивается код в файле. Можно удобно переходить между маршрутами. По клику на методе открывается подробная информация о параметрах, принимаемых сервером, и возможных вариантах ответа. И прямо тут можно протестировать каждый метод. Нажимаем кнопку Try this operation
, вводим имя в поле name
, кликаем на Send Request
и смотрим на ответ сервера:
Теперь подробнее рассмотрим yaml-файл.
В самом его начале указывается формат запросов и ответов:
# формат тела запроса, который клиент может отправить (Content-Type)
consumes:
- application/json
# формат ответа клиенту (Accepts)
produces:
- application/json
Далее следует описание маршрута:
paths:
/hello:
x-swagger-router-controller: hello_world
get:
description: Returns 'Hello' to the caller
# метод контроллера
operationId: hello
parameters:
- name: name
in: query
description: The name of the person to whom to say hello
required: false
type: string
responses:
"200":
description: Success
schema:
# ссылка на определение
$ref: "#/definitions/HelloWorldResponse"
# могут быть и ошибки
default:
description: Error
schema:
$ref: "#/definitions/ErrorResponse"
Здесь:
x-swagger-router-controller
- название файла-контроллера (/api/controllers/hello_world.js
);operationId
- функция в этом контроллере, которая обрабатывает данный запрос;parameters
- перечень необходимых параметров. В нашем случае только один необязательный (required: false
) строковый (type: string
) параметрname
, который должен находиться в строке запроса (in: query
);responses
- список возможных ответов сервера. Для статуса "200" через ссылку$ref
определена схемаHelloWorldResponse
, а для ошибки -ErrorResponse
. Данные определения располагаются в конце файла.
Еще взглянем на функцию hello
в контроллере:
function hello(req, res) {
var name = req.swagger.params.name.value || 'stranger';
var hello = util.format('Hello, %s!', name);
res.json(hello);
}
Здесь стоит обратить внимание на то, что теперь у объекта req
есть свойство swagger
с информацией о параметрах.
Теперь, пожалуй, можно приступать к написанию своего API.
Включаем mock-режим
Для того чтобы сосредоточиться исключительно на написании самого API и не переключаться на написание кода, можно запустить сервер в mock-режиме:
swagger project start -m
Теперь сервер будет отдавать ответы, сгенерированные на основании описанных схем. Строку operationId
можно не указывать. Так для /hello
в этом режиме сервер вернет ответ:
{
"message": "Sample text"
}
GET /books
Данный роут должен возвращать список всех книг. По поводу пагинации и разбиения выдачи на части здесь заморачиваться не будем - это тема для отдельной статьи.
Удаляем пример с /hello
роутом и добавляем в paths
следующие строки:
/books:
x-swagger-router-controller: book-controller
get:
description: Return a books list
responses:
"200":
description: Success
schema:
type: array
items:
$ref: '#/definitions/Book'
default:
description: Error
schema:
$ref: "#/definitions/ErrorResponse"
Здесь мы указываем, что будем использовать контроллер book-controller
. Определяем две схемы для ответа сервера:
definitions:
Book:
properties:
id:
type: string
title:
type: string
author:
type: string
year:
type: integer
required:
- title
- author
- year
ErrorResponse:
properties:
message:
type: string
required:
- message
Эти определения располагаем в конце файла.
Сервер может вернуть нам либо список (type: array
) книг со статусом "200", либо ошибку с обязательным полем message
.
Попробуем протестировать наше API:
Вернулся автоматически сгенерированный ответ. Для поля, тип которого указан как строковый, возвращается строка "Sample text". Для целочисленного поля - число. Если данный вариант не устраивает, то в директории /api/mocks
можно создать свой mock-контроллер book-controller
и задать ссылку на его функцию в поле operationId
.
POST /books
Создадим роут для добавления новой книги. Следом за get
добавим следующие строки:
post:
description: Add a new book
parameters:
- name: book
description: Book properties
in: body
required: true
schema:
$ref: "#/definitions/Book"
responses:
"200":
description: Success
schema:
$ref: "#/definitions/GeneralResponse"
default:
description: Error
schema:
$ref: "#/definitions/ErrorResponse"
Также нам нужно определить форму ответа сервера в случае успешного добавления книги. В definitions
добавим:
GeneralResponse:
properties:
message:
type: string
required:
- message
Попробуем протестировать:
Как видим, редактор предлагает ввести параметры и указывает, какие поля обязательны для заполнения.
GET /books/{id}
Теперь попробуем получить одну книгу. Для этого нужно создать новый маршрут /books/{id}
, где id
- идентификатор книги - параметр, передаваемый в самом url запроса. В блок paths
добавим следующие строки:
/books/{id}:
x-swagger-router-controller: book-controller
get:
description: Return Book by id
parameters:
- name: id
type: string
in: path
required: true
responses:
"200":
description: Success
schema:
$ref: '#/definitions/Book'
default:
description: Error
schema:
$ref: "#/definitions/ErrorResponse"
PUT /books/{id}
Данный роут используется для изменения книги с id
, указанным в url запроса. Изменяемые параметры передаются в теле запроса.
put:
description: Update a book
parameters:
- name: id
description: Book id
type: string
in: path
required: true
- name: book
description: Book properties
in: body
required: true
schema:
$ref: "#/definitions/Book"
responses:
"200":
description: Success
schema:
$ref: "#/definitions/GeneralResponse"
default:
description: Error
schema:
$ref: "#/definitions/ErrorResponse"
DELETE /books/{id}
Последняя операция - удаление по заданному id
. Копипастим следующий код:
delete:
description: Delite a book by ID
parameters:
- name: id
description: Book id
type: string
in: path
required: true
responses:
"200":
description: Success
schema:
$ref: "#/definitions/GeneralResponse"
default:
description: Error
schema:
$ref: "#/definitions/ErrorResponse"
Ну вот мы и спроектировали API для нашей библиотеки. Теперь можно приступать к написанию кода.
Реализуем функционал API
Для хранения нашей коллекции книг будем использовать MongoDB. Убедитесь, что у вас она у вас установлена и запущена.
Для работы с базой данных установим Mongoose ODM:
npm install mongoose -save
В файле app.js
добавим зависимость:
var mongoose = require('mongoose');
и подключимся к базе books
:
SwaggerExpress.create(config, function(err, swaggerExpress) {
if (err) { throw err; }
mongoose.connect('mongodb://localhost/books');
...
Ниже можно удалить условие проверки пути /hello
. Для нас это больше не актуально.
Book model
В каталоге api
создадим директорию models
и разместим в ней файл с моделью книги book-model.js
:
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var bookSchema = new Schema({
title: String,
author: String,
year: Number
});
var Book = mongoose.model('Book', bookSchema);
module.exports = Book;
Book controller
В директории /api/controllers
создадим файл book-controller.js
:
var Book = require('../models/book-model');
module.exports = {
getAll: getAll,
create: create,
find: find,
update: update,
remove: remove
};
function getAll(req, res) {
Book.find({}, function (err, books) {
if (err) {
throw err;
} else {
res.json(books);
}
});
}
function create(req, res) {
var newBook = Book(req.swagger.params.book.value);
newBook.save(function (err) {
if (err) {
throw err
} else {
res.json({message: 'OK'});
}
});
}
function find(req, res) {
var bookId = req.swagger.params.id.value;
Book.findById(bookId, function (err, book) {
if (err) {
throw err;
} else if (!book) {
res.status(404).json({message: 'Book not found'})
} else {
res.json({title: book.title, author: book.author, year: book.year});
}
});
}
function update(req, res) {
var bookId = req.swagger.params.id.value;
var newBook = req.swagger.params.book.value;
Book.findByIdAndUpdate(bookId, newBook, function (err, book) {
if (err) {
throw err;
} else {
res.json({message: 'OK'});
}
});
}
function remove(req, res) {
var bookId = req.swagger.params.id.value;
Book.findByIdAndRemove(bookId, function (err) {
if (err) {
throw err;
} else {
res.json({message: 'OK'});
}
});
}
Осталось в редакторе swagger.yaml прописать для каждой операции свой operationId
, соответствующий названию функции в контроллере:
# в /books:
get:
operationId: getAll
post:
operationId: create
# в /books/{id}:
get:
operationId: find
put:
operationId: update
delete:
operationId: remove
и перезапустить сервер без mock-режима:
swagger project start
Скриншоты работы контроллеров и результаты ответов сервера уже не буду выкладывать. И так вышло многабукоф. Можете протестировать самостоятельно. Код данного примера можно найти на github. Комментарии приветствуются.