Статьи

RESTful API дизайн с NodeJS & Restify

Конечный продукт
Что вы будете создавать

RESTful API состоит из двух основных понятий: ресурс и представление . Ресурсом может быть любой объект, связанный с данными или идентифицированный с помощью URI (более одного URI может ссылаться на один и тот же ресурс), и он может работать с использованием методов HTTP. Представление — это способ отображения ресурса. В этом руководстве мы рассмотрим теоретическую информацию о дизайне API RESTful и реализуем пример API приложения для блогов с помощью NodeJS.

Выбор правильных ресурсов для RESTful API является важным разделом проектирования. Прежде всего, вам необходимо проанализировать бизнес-домен, а затем решить, сколько и какие ресурсы будут использоваться в соответствии с потребностями вашего бизнеса. Если вы разрабатываете API для ведения блогов, вы, вероятно, будете использовать Article , User и Comment . Это имена ресурсов, а связанные с ними данные — это сам ресурс:

01
02
03
04
05
06
07
08
09
10
11
{
    «title»: «How to Design RESTful API»,
    «content»: «RESTful API design is a very important case in the software development world.»,
    «author»: «huseyinbabal»,
    «tags»: [
        «technology»,
        «nodejs»,
        «node-restify»
        ]
    «category»: «NodeJS»
}

Вы можете приступить к операции с ресурсами после того, как определились с необходимыми ресурсами. Операция здесь относится к HTTP-методам. Например, чтобы создать статью, вы можете сделать следующий запрос:

01
02
03
04
05
06
07
08
09
10
POST /articles HTTP/1.1
Host: localhost:3000
Content-Type: application/json
 
{
  «title»: «RESTful API Design with Restify»,
  «slug»: «restful-api-design-with-restify»,
  «content»: «Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas.»,
  «author»: «huseyinbabal»
}

Таким же образом вы можете просмотреть существующую статью, выполнив следующий запрос:

1
2
3
GET /articles/123456789012 HTTP/1.1
Host: localhost:3000
Content-Type: application/json

Как насчет обновления существующей статьи? Я слышу, что вы говорите:

Я могу сделать еще один запрос POST к / article / update / 123456789012 с полезной нагрузкой.

Может быть предпочтительнее, но URI становится все более сложным. Как мы уже говорили ранее, операции могут ссылаться на методы HTTP. Это означает, что указывайте операцию обновления в методе HTTP, а не в URI. Например:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
PUT /articles/123456789012 HTTP/1.1
Host: localhost:3000
Content-Type: application/json
{
    «title»: «Updated How to Design RESTful API»,
    «content»: «Updated RESTful API design is a very important case in the software development world.»,
    «author»: «huseyinbabal»,
    «tags»: [
        «technology»,
        «nodejs»,
        «restify»,
        «one more tag»
        ]
    «category»: «NodeJS»
}

Кстати, в этом примере вы видите теги и поля категорий. Это не обязательные поля. Вы можете оставить их пустыми и установить их в будущем.

Иногда вам нужно удалить статью, если она устарела. В этом случае вы можете использовать УДАЛИТЬ HTTP-запрос к / article / 123456789012.

Методы HTTP являются стандартными понятиями. Если вы будете использовать их как операцию, у вас будут простые URI, и этот простой API поможет вам получить счастливых потребителей.

Что делать, если вы хотите вставить комментарий к статье? Вы можете выбрать статью и добавить новый комментарий к выбранной статье. Используя этот оператор, вы можете использовать следующий запрос:

1
2
3
4
5
6
7
POST /articles/123456789012/comments HTTP/1.1
Host: localhost:3000
Content-Type: application/json
{
    «text»: «Wow! this is a good tutorial»,
    «author»: «john doe»
}

Вышеуказанная форма ресурса называется подресурсом. Комментарий является под-ресурсом статьи. Содержимое комментария выше будет вставлено в базу данных как дочерний элемент Article . Иногда другой URI ссылается на один и тот же ресурс. Например, для просмотра конкретного комментария вы можете использовать:

1
2
3
GET /articles/123456789012/comments/123 HTTP/1.1
Host: localhost:3000
Content-Type: application/json

или:

1
2
3
GET /comments/123456789012 HTTP/1.1
Host: localhost:3000
Content-Type: application/json

В общем, функции API часто меняются, чтобы предоставлять новые функции потребителям. В этом случае две версии одного и того же API могут существовать одновременно. Чтобы разделить эти две функции, вы можете использовать управление версиями. Есть две формы управления версиями

  1. Версия в URI: Вы можете указать номер версии в URI. Например, /v1.1/articles/123456789012 .  
  2. Версия в заголовке: укажите номер версии в заголовке и никогда не меняйте URI.   Например:
1
2
3
GET /articles/123456789012 HTTP/1.1
Host: localhost:3000
Accept-Version: 1.0

На самом деле версия изменяет только представление ресурса, а не концепцию ресурса. Таким образом, вам не нужно менять структуру URI. В версии 1.1, возможно, новое поле было добавлено в статью. Тем не менее, он по-прежнему возвращает статью. Во втором варианте URI по-прежнему прост, и потребителям не нужно менять свой URI в реализациях на стороне клиента.

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

Представление — это способ, которым API отображает ресурс. Когда вы вызываете конечную точку API, вам будет возвращен ресурс. Этот ресурс может иметь любой формат, например XML, JSON и т. Д. JSON предпочтительнее, если вы разрабатываете новый API. Однако если вы обновляете существующий API, который использовал для возврата ответа XML, вы можете предоставить другую версию для ответа JSON.

Достаточно теоретической информации о дизайне RESTful API. Давайте посмотрим на реальное использование, спроектировав и реализовав API блогов с помощью Restify.

Чтобы разработать RESTful API, нам нужно проанализировать бизнес-область. Тогда мы можем определить наши ресурсы. В API блогов нам нужно:

  • Создать, обновить, удалить, просмотреть статью
  • Создать комментарий для конкретной статьи , Обновить, Удалить, Просмотреть, Комментарий
  • Создать, обновить, удалить, просмотреть пользователя

В этом API я не буду описывать, как аутентифицировать пользователя для создания статьи или комментария. Для части аутентификации вы можете обратиться к учебнику по аутентификации на основе токенов с AngularJS & NodeJS .

Имена наших ресурсов готовы. Ресурсные операции просто CRUD. Вы можете обратиться к следующей таблице для общей демонстрации API.

Название ресурса HTTP-глаголы Методы HTTP
Статья создать статью
обновить статью
удалить статью
просмотреть статью
POST / статьи с полезной нагрузкой
PUT / статьи / 123 с полезной нагрузкой
УДАЛИТЬ / статьи / 123
GET / article / 123
Комментарий создать комментарий
обновить Coment
удалить комментарий
просмотреть комментарий
POST / статьи / 123 / комментарии с полезной нагрузкой
PUT / comments / 123 с полезной нагрузкой
УДАЛИТЬ / комментарии / 123
GET / comments / 123
пользователь Создать пользователя
обновить пользователя
удалить пользователя
просмотр пользователя
POST / пользователи с полезной нагрузкой
PUT / users / 123 с полезной нагрузкой
УДАЛИТЬ / users / 123
GET / users / 123

В этом проекте мы будем использовать NodeJS с Restify . Ресурсы будут сохранены в базе данных MongoDB . Прежде всего, мы можем определить ресурсы как модели в Restify.

01
02
03
04
05
06
07
08
09
10
11
12
13
var mongoose = require(«mongoose»);
var Schema = mongoose.Schema;
 
var ArticleSchema = new Schema({
    title: String,
    slug: String,
    content: String,
    author: {
        type: String,
        ref: «User»
    }
});
mongoose.model(‘Article’, ArticleSchema);
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
var mongoose = require(«mongoose»);
var Schema = mongoose.Schema;
 
var CommentSchema = new Schema({
    text: String,
    article: {
        type: String,
        ref: «Article»
    },
    author: {
        type: String,
        ref: «User»
    }
});
mongoose.model(‘Comment’, CommentSchema);

Не будет никакой операции для ресурса пользователя. Предположим, что мы уже знаем текущего пользователя, который сможет работать со статьями или комментариями.

Вы можете спросить, откуда этот модуль Мангуста. Это самая популярная платформа ORM для MongoDB, написанная в виде модуля NodeJS. Этот модуль включен в проект в другом конфигурационном файле.

Теперь мы можем определить наши HTTP-глаголы для вышеуказанных ресурсов. Вы можете увидеть следующее:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
var restify = require(‘restify’)
    , fs = require(‘fs’)
 
 
var controllers = {}
    , controllers_path = process.cwd() + ‘/app/controllers’
fs.readdirSync(controllers_path).forEach(function (file) {
    if (file.indexOf(‘.js’) != -1) {
        controllers[file.split(‘.’)[0]] = require(controllers_path + ‘/’ + file)
    }
})
 
var server = restify.createServer();
 
server
    .use(restify.fullResponse())
    .use(restify.bodyParser())
 
// Article Start
server.post(«/articles», controllers.article.createArticle)
server.put(«/articles/:id», controllers.article.updateArticle)
server.del(«/articles/:id», controllers.article.deleteArticle)
server.get({path: «/articles/:id», version: «1.0.0»}, controllers.article.viewArticle)
server.get({path: «/articles/:id», version: «2.0.0»}, controllers.article.viewArticle_v2)
// Article End
 
// Comment Start
server.post(«/comments», controllers.comment.createComment)
server.put(«/comments/:id», controllers.comment.viewComment)
server.del(«/comments/:id», controllers.comment.deleteComment)
server.get(«/comments/:id», controllers.comment.viewComment)
// Comment End
 
var port = process.env.PORT ||
server.listen(port, function (err) {
    if (err)
        console.error(err)
    else
        console.log(‘App is ready at : ‘ + port)
})
 
if (process.env.environment == ‘production’)
    process.on(‘uncaughtException’, function (err) {
        console.error(JSON.parse(JSON.stringify(err, [‘stack’, ‘message’, ‘inner’], 2)))
    })

В этом фрагменте кода прежде всего итерируются файлы контроллеров, которые содержат методы контроллеров, и все контроллеры инициализируются для выполнения определенного запроса к URI. После этого URI для определенных операций определяются для основных операций CRUD. Существует также версия для одной из операций над статьей.

Например, если вы viewArticle_v2 версию как 2 в заголовке Accept-Version, будет выполнен viewArticle_v2 . viewArticle и viewArticle_v2 оба выполняют viewArticle_v2 и ту же работу, показывая ресурс, но они показывают ресурс Article в другом формате, как вы можете видеть в поле title ниже. Наконец, сервер запускается на определенном порту, и применяются некоторые проверки отчетов об ошибках. Мы можем перейти к методам контроллера для операций HTTP над ресурсами.

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
var mongoose = require(‘mongoose’),
    Article = mongoose.model(«Article»),
    ObjectId = mongoose.Types.ObjectId
 
exports.createArticle = function(req, res, next) {
    var articleModel = new Article(req.body);
    articleModel.save(function(err, article) {
        if (err) {
            res.status(500);
            res.json({
                type: false,
                data: «Error occured: » + err
            })
        } else {
            res.json({
                type: true,
                data: article
            })
        }
    })
}
 
exports.viewArticle = function(req, res, next) {
    Article.findById(new ObjectId(req.params.id), function(err, article) {
        if (err) {
            res.status(500);
            res.json({
                type: false,
                data: «Error occured: » + err
            })
        } else {
            if (article) {
                res.json({
                    type: true,
                    data: article
                })
            } else {
                res.json({
                    type: false,
                    data: «Article: » + req.params.id + » not found»
                })
            }
        }
    })
}
 
exports.viewArticle_v2 = function(req, res, next) {
    Article.findById(new ObjectId(req.params.id), function(err, article) {
        if (err) {
            res.status(500);
            res.json({
                type: false,
                data: «Error occured: » + err
            })
        } else {
            if (article) {
                article.title = article.title + » v2″
                res.json({
                    type: true,
                    data: article
                })
            } else {
                res.json({
                    type: false,
                    data: «Article: » + req.params.id + » not found»
                })
            }
        }
    })
}
 
exports.updateArticle = function(req, res, next) {
    var updatedArticleModel = new Article(req.body);
    Article.findByIdAndUpdate(new ObjectId(req.params.id), updatedArticleModel, function(err, article) {
        if (err) {
            res.status(500);
            res.json({
                type: false,
                data: «Error occured: » + err
            })
        } else {
            if (article) {
                res.json({
                    type: true,
                    data: article
                })
            } else {
                res.json({
                    type: false,
                    data: «Article: » + req.params.id + » not found»
                })
            }
        }
    })
}
 
exports.deleteArticle = function(req, res, next) {
    Article.findByIdAndRemove(new Object(req.params.id), function(err, article) {
        if (err) {
            res.status(500);
            res.json({
                type: false,
                data: «Error occured: » + err
            })
        } else {
            res.json({
                type: true,
                data: «Article: » + req.params.id + » deleted successfully»
            })
        }
    })
}

Вы можете найти объяснение основных операций CRUD на стороне Mongoose ниже:

  • createArticle: это простая операция сохранения для articleModel отправляемая из тела запроса. Новую модель можно создать, передав тело запроса в качестве конструктора модели, такой как var articleModel = new Article(req.body) .
  • viewArticle: для просмотра сведений о статье в параметре URL требуется идентификатор статьи. findOne с параметром ID достаточно, чтобы вернуть подробности статьи.
  • updateArticle: обновление статьи — это простой запрос поиска и некоторые манипуляции с данными в возвращаемой статье. Наконец, обновленная модель должна быть сохранена в базе данных с помощью команды save .
  • deleteArticle: findByIdAndRemove — лучший способ удалить статью, предоставив идентификатор статьи.

Упомянутые выше команды Mongoose — это просто статический метод через объект Article, который также является ссылкой на схему Mongoose.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
var mongoose = require(‘mongoose’),
    Comment = mongoose.model(«Comment»),
    Article = mongoose.model(«Article»),
    ObjectId = mongoose.Types.ObjectId
 
exports.viewComment = function(req, res) {
    Article.findOne({«comments._id»: new ObjectId(req.params.id)}, {«comments.$»: 1}, function(err, comment) {
        if (err) {
            res.status(500);
            res.json({
                type: false,
                data: «Error occured: » + err
            })
        } else {
            if (comment) {
                res.json({
                    type: true,
                    data: new Comment(comment.comments[0])
                })
            } else {
                res.json({
                    type: false,
                    data: «Comment: » + req.params.id + » not found»
                })
            }
        }
    })
}
 
exports.updateComment = function(req, res, next) {
    var updatedCommentModel = new Comment(req.body);
    console.log(updatedCommentModel)
    Article.update(
        {«comments._id»: new ObjectId(req.params.id)},
        {«$set»: {«comments.$.text»: updatedCommentModel.text, «comments.$.author»: updatedCommentModel.author}},
        function(err) {
            if (err) {
                res.status(500);
                res.json({
                    type: false,
                    data: «Error occured: » + err
                })
            } else {
                res.json({
                    type: true,
                    data: «Comment: » + req.params.id + » updated»
                })
            }
    })
}
 
exports.deleteComment = function(req, res, next) {
    Article.findOneAndUpdate({«comments._id»: new ObjectId(req.params.id)},
        {«$pull»: {«comments»: {«_id»: new ObjectId(req.params.id)}}},
        function(err, article) {
        if (err) {
            res.status(500);
            res.json({
                type: false,
                data: «Error occured: » + err
            })
        } else {
            if (article) {
                res.json({
                    type: true,
                    data: article
                })
            } else {
                res.json({
                    type: false,
                    data: «Comment: » + req.params.id + » not found»
                })
            }
        }
    })
}

Когда вы делаете запрос к одному из URI ресурса, будет выполняться соответствующая функция, указанная в контроллере. Каждая функция внутри файлов контроллера может использовать объекты req и res . Ресурс комментариев здесь является подресурсом Статьи. Все операции запроса выполняются через модель Article, чтобы найти вложенный документ и внести необходимые изменения. Однако всякий раз, когда вы пытаетесь просмотреть ресурс Comment, вы увидите его, даже если в MongoDB нет коллекции.

  • Выберите легкие для понимания ресурсы, чтобы обеспечить удобство использования для потребителей.
  • Пусть бизнес-логика будет реализована потребителями. Например, ресурс Article имеет поле с именем slug. Потребителям не нужно отправлять эту информацию в REST API. Эта стратегия слагов должна быть на стороне REST API, чтобы уменьшить связь между API и потребителями. Потребителям нужно только отправить подробные данные о названии, и вы можете создать слаг в соответствии со своими бизнес-потребностями на стороне REST API.
  • Реализуйте уровень авторизации для ваших конечных точек API. Неавторизованные потребители могут получить доступ к ограниченным данным, принадлежащим другому пользователю. В этом руководстве мы не рассмотрели ресурс пользователя, но вы можете обратиться к Token Based Authentication с AngularJS & NodeJS для получения дополнительной информации об аутентификации API.
  • URI пользователя вместо строки запроса. /articles/123 (хорошо), /articles?id=123 (плохо).
  • Не держи государство; всегда используйте мгновенный ввод / вывод.
  • Используйте существительное для своих ресурсов. Вы можете использовать методы HTTP для работы с ресурсами.

Наконец, если вы разрабатываете RESTful API, следуя этим фундаментальным правилам, у вас всегда будет гибкая, легко обслуживаемая и понятная система.