Так зачем же нужен этот топик, ведь в сети огромное множество материалов о том, как построить MVC приложения на основе Express? Например, вот эта статья и многие другие. Весь их недостаток для нас в том, что они описывают MVC приложения, которые по архитектуре отличаются от REST приложений. И вот тут у начинающего разработчика возникает ряд вопросов:
- А нужны ли нам теперь view файлы?
- А нужны ли нам теперь controllers и если да, то как они будут модифицированы?
- Как теперь организовать структуру файлов?
- и так далее…
Так почему же именно Express, ведь есть и другие фреймворки (Restify, например), которые созданы специально для REST приложений? Потому что именно он имеет наибольшее сообщество в сети, и вероятность столкнуться с проблемой, которую никто раньше не встречал, — крайне мала. На начальном этапе это довольно важно и спасет ваше драгоценное время. В дальнейшем никто не мешает вам начать использовать что-то более специфическое, но сейчас остановимся именно на Express.
Ну, хватит теории, приступим! Для тех, кто только начинает работать с Express или Mongoose, рекомендую ознакомиться с документацией (Express и Mongoose).
Файловая структура нашего проекта будет такова:
/config main.json /handlers entities.js /libs config.js crudHandlers mongoose.js /models entities.js /package.json /routes.js /server.js
package.json
{ "name": "node-rest-express-seed", "version": "0.0.1", "dependencies": { "express": "~3.4.8", "nconf": "~0.6.9", "winston": "~0.7.2", "mongoose": "~3.8.6", "underscore": "~1.5.2", "async": "~0.2.10" } }
Здесь мы описали несколько модулей, предназначение которых поясню:
- Nconf — модуль для работы с конфигурацией.
- Winston — модуль для работы с выводом логов.
- Underscore — модуль с набором полезных методов для работы с обектами, массивами, коллекциями.
- Async — модуль, помогающий избежать «callback hell» и умеещий управлять асинхронными операциями самыми разными способами.
server.js
var express = require('express'); var path = require('path'); var winston = require('winston'); var routes = require('./routes'); // Файл с роутам var config = require('./libs/config'); // Используемая конфигурация var db = require('./libs/mongoose'); // Файл работы с базой MongoDB var app = express(); // Создаем обьект express app.use(express.json()); // "Обучаем" наше приложение понимать JSON и urlencoded запросы app.use(express.urlencoded()); app.use(express.methodOverride()); // Переопределяем PUT и DELETE запросы для работы с WEB формами // Если произошла ошибка валидации, то отдаем 400 Bad Request app.use(function (err, req, res, next) { console.log(err.name); if (err.name == "ValidationError") { res.send(400, err); } else { next(err); } }) // Если же произошла иная ошибка то отдаем 500 Internal Server Error app.use(function (err, req, res, next) { res.send(500, err); }); // Инициализируем Handlers var handlers = { entities: require('./handlers/entities') } // Метод запуска нашего сервера function run() { routes.setup(app, handlers); // Связуем Handlers с Routes app.listen(config.get('port'), function () { // Сервер запущен winston.info("App running on port:" + config.get('port')); }); db.init(path.join(__dirname, "models"), function (err, data) { //Выводим сообщение об успешной инициализации базы данных winston.info("All the models are initialized"); }); } if (module.parent) { //Если server.js запущен как модуль, то отдаем модуль с методом run module.exports.run = run; } else { //Иначе стартуем сервер прямо сейчас run(); }
Код обильно задокументирован, и все должно быть понятно, кроме Handlers и Routes. О них я расскажу немного позже.
libs/config.js
// 1. Аргументы командной строки // 2. Переменные среды // 3. Наш собственный файл с конфигурацией var nconf = require('nconf'); nconf.argv() .env() .file({ file: './config/main.json' }); module.exports = nconf;
И соответственно сам файл конфигурации
config/main.json
{ "port" : 1337, "mongoose": { "uri": "mongodb://localhost/habr" } }
Здесь немного настроек (пока). Мы лишь опишем, на каком порту будет крутиться наше приложение и параметры для MongoDB
Routes and Handlers
Мы ввели два новых понятия, сейчас поясню, зачем они нам нужны. Для этого необходимо вспомнить, как Express обрабатывает запросы к нашему приложению. Если говорить в общем, то Express ищет обработчик для данного API адреса, и если результат положительный — передает данные запроса в него. По сути Route — это:
app.get('/v1/blabla',someCoolFunctionForBlaBla);
Его задача пробросить данные от пользователя во внутренний обработчик — Handler. Пример такого обработчика:
var someCoolFunctionForBlaBla = function(req,res,next) { // Тут мы берем из объекта req все нам необходимое, например, req.query или req.body // Проводим все необходимые операции и вычисления res.send("Hello") // Отдаем данные пользователю, например, строку "Hello" };
Routes мы будем хранить в одном файле, а вот Handlers будем хранить в других. Это даст нам разделение логики обработчиков от их интерфейсов.
routes.js
module.exports.setup = function (app, handlers) { app.get('/v1/entities', handlers.entities.list); app.get('/v1/entities/:id', handlers.entities.get); app.post('/v1/entities', handlers.entities.create); app.put('/v1/entities/:id', handlers.entities.update); app.delete('/v1/entities/:id', handlers.entities.remove); };
Здесь все должно быть понятно, если нет — нужно еще раз перечитать документацию Express.
handlers/entities.js
var mongoose = require('../libs/mongoose'); // Выставляем modelName var modelName = 'entities'; // Подгружаем стандартные методы для CRUD документов var handlers = require('../libs/crudHandlers')(modelName); module.exports = handlers;
Я думаю, здесь вы предполагали увидеть больше кода, чем оказалось. Поясню. Так как CRUD для сущностей базы данных часто имеет одну и ту же логику, то можно вынести это в отдельный модуль. Если же нам нужна иная логика, то можно просто переопределить методы из crudHandlers в entities.js или вовсе не использовать его.
libs/crudHandlers.js
var mongoose = require('mongoose'); var db = require('./mongoose'); module.exports = function (modelName) { // Список документов var list = function (req, res, next) { db.model(modelName).find({}, function (err, data) { if (err) next(err); res.send(data); }); }; // Один документ var get = function (req, res, next) { try{var id = mongoose.Types.ObjectId(req.params.id)} catch (e){res.send(400)} db.model(modelName).find({_id: id}, function (err, data) { if (err) next(err); if (data) { res.send(data); } else { res.send(404); } }) }; // Создаем документ var create = function (req, res, next) { db.model(modelName).create(req.body, function (err, data) { if (err) { next(err); } res.send(data); }); }; // Обновляем документ var update = function (req, res, next) { try{var id = mongoose.Types.ObjectId(req.params.id)} catch (e){res.send(400)} db.model(modelName).update({_id: id}, {$set: req.body}, function (err, numberAffected, data) { if (err) next(err); if (numberAffected) { res.send(200); } else { res.send(404); } }) }; // Удаляем документ var remove = function (req, res, next) { try{var id = mongoose.Types.ObjectId(req.params.id)} catch (e){res.send(400)} db.model(modelName).remove({_id: id}, function (err, data) { if (err) next(err); res.send(data ? req.params.id : 404); }); }; return { list : list, get : get, create: create, update: update, remove: remove } };
Здесь мы лишь описали CRUD методы, они могут быть у вас, какие вам заблагорассудятся. Но что же такое db.model? Посмотрим подробней:
libs/mongoose.js
var mongoose = require('mongoose'); var fs = require('fs'); var path = require('path'); var async = require('async'); var config = require('./config'); mongoose.connect(config.get('mongoose:uri')); var db = mongoose.connection; db.on('error', function (err) { // Обрабатываем ошибку }); db.once('open', function callback() { // Соединение прошло успешно }); var models = {}; //Инициализируем все схемы var init = function (modelsDirectory, callback) { //Считываем список файлов из modelsDirectory var schemaList = fs.readdirSync(modelsDirectory); //Создаем модели Mongoose и вызываем callback, когда все закончим async.eachSeries(schemaList, function (item, cb) { var modelName = path.basename(item, '.js'); models[modelName] = require(path.join(modelsDirectory, modelName))(mongoose); cb(); }, callback); }; //Возвращаем уже созданные модели из списка var model = function (modelName) { var name = modelName.toLowerCase(); if (typeof models[name] == "undefined") { // Если модель на найдена, то создаем ошибку throw "Model '" + name + "' is not exist"; } return models[name]; }; module.exports.init = init; module.exports.model = model;
Для каждой сущности в БД у нас будет отдельная модель, и чтобы каждый раз, когда она нам понадобится, не делать require, нам и нужен этот файл.
Теперь осталось только описать модель entities:
models/entities.js
var path = require('path'); module.exports = function (mongoose) { //Объявляем схему для Mongoose var Schema = new mongoose.Schema({ name: { type: String, required: true } }); // Инициализируем модель с именем файла, в котором она находится return mongoose.model(path.basename(module.filename, '.js'), Schema); };
Ну, вот и все, можно запускать.
Исходники всего этого — вот.
Установить с github:
git clone https://github.com/asynxis/node-rest-seed.git my-firts-app cd my-firts-app npm i node server.js
После запуска вы должны увидеть такие строки:
info: All the models are initialized info: App running on port:1337
Если этого не произошло, проверьте, запущена ли у вас MongoDB и свободен ли у вас порт 1337.
Использование
После запуска вы получаете REST сервер с такими возможностями:
- GET /v1/entities — список
- GET /v1/entities/:id — получаем запись с ключом id
- POST /v1/entities — создаем (нужно только передать name в параметрах)
- PUT /v1/entities/:id — обновляем по id (обновляемые поля также передаем в параметрах)
- DELETE /v1/entities/:id- удаляем по id
Например, вы добавили новую сущность в БД и хотите сделать CRUD к ней. Для этого нужно проделать 4 простые вещи:
- Создаем файл с именем вашей новой сущности в директори handlers и определяем там modelName.
- Создаем файл c именем modelName в директории models и определяем там Mongoose Schema для новой сущности.
- Обновляем объект handlers в server.js.
- Добавляем нужные нам роуты в routes.js
И, вуаля, у нас есть CRUD к еще одной сущности!
Теперь можно начинать делать из этого каркаса ваше супер приложение!
P.S. Буду рад комментариям и исправлениям, сам тоже использую NodeJS недавно. Хочется помочь тем, кто также, как и я, начинает!
ссылка на оригинал статьи http://habrahabr.ru/post/213931/
Добавить комментарий