Данный топик ориентирован больше на начинающих NodeJS разработчиков. Когда я говорю о начинающем разработчике, то понимаю под этим, что некоторые знания о NodeJS у вас уже имеются. Иначе настоятельно рекомендую сначала ознакомиться с основами (например, здесь) и лишь потом вернуться к этой статье.
Так зачем же нужен этот топик, ведь в сети огромное множество материалов о том, как построить 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 недавно. Хочется помочь тем, кто также, как и я, начинает!
Автор: asynxis