Russian (Pусский) translation by Anton Lisovsky (you can also view the original English article)

Restful API состоит из двух основных концепций: Ресурс (Resource) и Представление (Representation). Ресурс может представлять из себя любой объект ассоциированный с данными или определён в качестве URI (больше чем один URI может ссылаться на один и тот же ресурс) и управлять им можно используя HTTP методы. Представление - это способ, которым вы отображаете ресурс. В данном туториале мы разберём теоретическую информацию касательно дизайна RESTful API и реализуем пример API приложения - блога, используя NodeJS.
Ресурс
Выбор правильных ресурсов для RESTful API является важной частью дизайна. Для начала, вам следует определиться со сферой вашего бизнеса и решить как много и каких ресурсов будут использованы, требуются вашему бизнесу. В случае, если речь идёт о дизайне API блога, вы скорее всего будете использовать Article, User и Comment. Это были названия ресурсов, данные которые ассоциированы с этими названиями, сами по себе являются ресурсами:
{ "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 методы. К примеру, для создания статьи вы можете воспользоваться следующим запросом:
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" }
Таким же способом, вы можете просмотреть существующую статью, выполнив следующий запрос:
GET /articles/123456789012 HTTP/1.1 Host: localhost:3000 Content-Type: application/json
Что насчёт обновления существующей статьи? Я могу слышать, как вы говорите:
Я могу сделать очередной POST запрос на /articles/update/123456789012.
Может быть и неплохой вариант, но URI станет более громоздким. Как мы упоминали ранее, действия могут представлять из себя HTTP методы. Это означает, лучше выбрать updte метод вместо того, чтобы использовать URI. К примеру:
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" }
Кстати в данном примере вы можете заметить поля тегов и категорий. Данные поля не обязательны. Вы можете оставить их пустыми и установить значение в будущем.
Иногда вам понадобится удалить статью если она устарела. В этом случае используйте DELETE HTTP запрос для /articles/123456789012.
Методы HTTP - стандартная концепция. Используя их в качестве действий, вы будете обладать не сложным URI, пользователи оценят подобное простое API.
Что если вам захочется добавить комментарий в статью? Выберите необходимую статью и добавьте новый комментарий. Для этого подойдёт следующий запрос:
POST /articles/123456789012/comments HTTP/1.1 Host: localhost:3000 Content-Type: application/json { "text": "Wow! this is a good tutorial", "author": "john doe" }
Форма ресурса выше называется под-ресурсом (sub-recource). Комментарий (Comment) под-ресурс Статьи (Article). Комментарий выше будет добавлен в базу данных как потомок Статьи. Иногда различные URI ссылаются на один и тот же ресурс. К примеру, для просмотра комментария вы можете использовать:
GET /articles/123456789012/comments/123 HTTP/1.1 Host: localhost:3000 Content-Type: application/json
или:
GET /comments/123456789012 HTTP/1.1 Host: localhost:3000 Content-Type: application/json
Версии
В основном API часто меняются, чтобы предоставить клиентам новый функционал. Поэтому, одновременно, могут существовать две версии API. Для того чтобы разделить два функционала вы можете использовать разные версии. Имеются две формы версий.
- Версия в URI: можно предоставить номер версии в URI. К примеру,
/v1.1/articles/123456789012
. - Версия в хедере: URI остаётся без изменений, однако в хедере передаётся номер версии, к примеру:
GET /articles/123456789012 HTTP/1.1 Host: localhost:3000 Accept-Version: 1.0
Вообще версия меняет только представление ресурса, но не саму концепцию ресурса. Поэтому вам не нужно менять структуру URI. Предположим в v1.1 было добавлено новое поле для статьи. Однако возвращается всё также статья. Используя вторую опцию, URI остаётся простым и пользователю не приходится менять URI на стороне клиента.
Важно учесть случаи когда клиент не передаёт номер версии. Можно показать сообщение об ошибке или вернуть ответ используя первую версию. В том случае если по умолчанию используется последняя стабильная версия, пользователь в результате может получить огромное количество ошибок со стороны реализации на клиенте.
Представление
Представление - способ, которым API отображает ресурсы. Когда осуществляется обращение к API возвращается ресурс. Данный ресурс может быть каким угодно форматом, к примеру XML, JSON и так далее, JSON предпочтительнее, в случае если речь идёт о новом API. Однако, если обновляется существующее API, которое уже реализовано с XML форматом, можно предоставить другую версию с ответами формата JSON.
Достаточно теории о дизайне RESTful API. Давайте разберём пример из реальной жизни, разработаем дизайн и реализацию API блога с помощью Restify.
REST API блога
Дизайн
Для того чтобы создать дизайн RESTful API, нам нужно проанализировать сферу деятельности нашего приложения. Тем самым мы будем в состоянии определить необходимые ресурсы. Для API блога, нам нужно:
- Возможность создавать, обновлять, удалять, просматривать статью (Article)
- Создать комментарий для какой-либо статьи, обновить, удалить, просмотреть комментарий (Comment)
- Создавать, обновлять, удалять, просматривать пользователя (User)
Для данного API я не буду рассматривать аутентификацию пользователя для создания статьи или комментария. Чтобы ознакомится с реализацией аутентификации взгляните на туториал Аутентификация основанная на токенах с AngularJS и NodeJS.
Названия ресурсов готовы. Действия совершаемые над ресурсами представляют обычный CRUD. Ознакомьтесь с таблицей ниже, чтобы иметь представление о нашем API.
Название ресурса | HTTP действие | HTTP методы |
---|---|---|
Article | create Article update Article delete Article view Article | POST /articles с данными PUT /articles/123 c данными DELETE /articles/123 GET /article/123 |
Comment | create Comment update Coment delete Comment view Comment | POST /articles/123/comments с данными PUT /comments/123 с данными DELETE /comments/123 GET /comments/123 |
User | create User update User delete User view User | POST /users с данными PUT /users/123 с данными DELETE /users/123 GET /users/123 |
Настройка проекта
В данном проекте мы будем использовать NodeJS с Restify. Ресурсы будут сохранятся в базе данных MongoDB. Для начала мы можем определить наши ресурсы, как Restify модели.
Статья
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);
Комментарий
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);
Пользователь
Для ресурса User не будет никаких действий. Предполагается, что нам уже известен текущий пользователь, который будет взаимодействовать со статьями.
Вы можете спросить откуда добавляется модуль mongoose. Это самый популярный ORM фреймворк для MongoDB - модуль написанный на NodeJS. Данный модуль добавляется в проект в другом файле с конфигурацией.
Теперь мы можем определить HTTP действия для ресурсов описанных выше. Можно заметить следующее:
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 || 3000; 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 действия. Также для одного действия с Article доступны разные версии.
К примеру, если установлена версия 2
в хедере Accept-Version, будет выполнен viewArticle_v2
. viewArticle
и viewArticle_v2
оба делают тоже самое, показывают ресурс, но в разных форматах, как можно заметить в поле title
. Наконец, сервер запущен на определённом порту, добавлена проверка ошибок. Мы можем продолжить работу с методами контроллера для HTTP действий ресурсов.
article.js
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: простое сохранение (save) для
articleModel
посылаемое из тела запроса. Новая модель может быть создана передавая тело запроса, как конструктор для модели, такой какvar articleModel = newArticle(req.body)
. - viewArticle: чтобы увидеть детали статьи, в качестве URL параметра нужен ID статьи.
findOne
с ID параметром, достаточно для получения деталей. - updateArtcle: обновление статьи обычный запрос поиска и манипуляция с данными возвращённой статьи. Наконец, необходимая, обновлённая модель должна быть сохранена в базе данных с помощью команды
save
. - deleteArticle:
findByIdAndRemove
- лучший способ удалить статьи согласно переданному ID.
Команды Mongoose упомянутые ранее - статичны, как объект Article, который также является ссылкой на схему Mongoose.
comment.js
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 объекты. Ресурс comment является под-ресурсом Article. Все действия запроса осуществляются через model статьи, чтобы найти под-документ и сделать необходимые обновления. Однако, когда вы пытаетесь посмотреть ресурс комментария, вам удастся его увидеть, даже если в MongoDB нет коллекции.
Другие рекомендации по дизайну
- Выбирайте легкий для понимания ресурс, тем самым вы облегчите процесс использования клиенту.
- Пускай бизнес логика будет реализована клиентом. К примеру, ресурс статьи имеет поле slug. Клиенту не нужно отправлять данную информацию REST API. Всё что связанно со slug должно быть обработано на стороне REST API, чтобы уменьшить сопряжение (coupling) между API и клиентом. Клиенту нужно только посылать детали заголовка и вы можете создать slug согласно бизнес потребностям на стороне REST API.
- Реализуйте уровень авторизации для конечных точек API. Неавторизированные клиенты могут получить доступ только к ограниченным данным, которые принадлежат другим пользователям. В данном туториале мы не рассмотрели ресурс User, но вы можете ознакомится с Аутентификация основанная на токенах с AngularJS и NodeJS для получения информации о API аутентификации.
- User URI вместо строки запроса.
/article/123
(хорошо),/articles?id=12
3 (плохо). - Не сохраняйте состояние; всегда используйте мгновенный ввод/вывод.
- Используйте существительные для ресурсов. Можно использовать методы HTTP для взаимодействия с ресурсами.
Ну и наконец, если разрабатывать RESTful API и следовать данным правилам, у вас всегда будет гибкая, простая в поддержке и понимании система.
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Update me weeklyEnvato Tuts+ tutorials are translated into other languages by our community members—you can be involved too!
Translate this post