Пишем 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 Editor

Слева отображается собственно наш файл swagger.yaml с возможностью его редактирования. А вот справа находится самое интересное - красивый список маршрутов и методов. Их можно сворачивать/разворачивать и при этом сворачивается/разворачивается код в файле. Можно удобно переходить между маршрутами. По клику на методе открывается подробная информация о параметрах, принимаемых сервером, и возможных вариантах ответа. И прямо тут можно протестировать каждый метод. Нажимаем кнопку Try this operation, вводим имя в поле name, кликаем на Send Request и смотрим на ответ сервера:

Swagger Editor Response

Теперь подробнее рассмотрим 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:

Books GET Response

Вернулся автоматически сгенерированный ответ. Для поля, тип которого указан как строковый, возвращается строка "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

Попробуем протестировать:

Swagger Editor POST

Как видим, редактор предлагает ввести параметры и указывает, какие поля обязательны для заполнения.

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. Комментарии приветствуются.

Павел Прудников

Постигающий дзен фулстэк вэб буддизма

Минск, Беларусь

Подписаться на Блог MEAN stack разработчика

Получайте свежие записи прямо на ваш почтовый ящик.

Или подпишитесь на RSS с Feedly!

Комментарии

comments powered by Disqus