Пишем RESTful API на Node.js с использованием Swagger

Можно разработать просто лучший 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. Комментарии приветствуются.