Базовое Node.JS приложение с использованием express

Всем привет.
Искал статью, как сделать базовое Node.JS приложение с использованием express, точнее какая базовая структура должна быть у проекта, но так ничего похожего для меня не нашел.
Потому решил написать собственную, дабы объяснить таким же как и я как это сделать и как это должно выглядеть.

Подробности под катом. Осторожно. Много текста и кода.

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

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

Итак. Начнем, пожалуй, с используемых модулей внутри приложения:

express - базовый пакет, для создания http-сервера
mongoose - фреймверк, для удобной работы с MongoDB
mongodb - native-driver для работы с MongoDB напрямую
connect-mongo - нужно для работы express с session
node-uuid - для генерирования токенов для авторизации (в случае использования веб-сервисов)
async - для работы с цепочкой асинхронных вызовов, ака Promise
ejs-locals - движок рендеринга, который поддерживает наследование шаблонов
nconf - для удобной работы с настройками приложения (собственный config.json)
string - для более удобной работы со строками, также очистка строк от ненужных вещей, типа html тегов и тд
validator - валидация данных
winston - для продвинутого логирования ошибок


Каждый из модулей можно установив используя команду:
npm install <<module_name>> --save

--save нужен для сохранения модуля в dependency (package.json), для дальнейшего развертывания приложения на других машинах.

Структура приложения получается следующей:

	/config
		config.json
		index.js
	/middleware
		checkAuth.js
		errorHandler.js
		index.js
	/models
		user.js
	/public
		/*JS, CSS, HTML static files*/
	/routes
		authentication.js
		error.js
		index.js
		main.js
		register.js
	/utils
		index.js
		log.js
		mongoose.js
		validate.js
	/views
		index.ejs
	manage.js
	package.json
	server.js


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

server.js


var express = require('express'),
    http = require('http'),
    app = express(),
    middleware = require('./middleware')(app, express),
    config = require('./config'),
    log = require('./utils/log')(app, module);

http.createServer(app).listen(config.get('port'), function(){
    log.info('Express server listening on port ' + config.get('port'));
});


В server.js создаем приложение epxress app, подключаем модуль middleware, в котором подключаются все нужные middleware приложения.
Дальше создаем сервер, который будет обрабатывать все входящие подключения через порт, который указан в конфиге.

package.json


{
  "name": "test_express_app",
  "version": "0.0.1",
  "private": true,
  "scripts": {
    "start": "node server.js"
  },
  "dependencies": {
    "express": "~3.4.6",
    "mongoose": "~3.8.1",
    "node-uuid": "~1.4.1",
    "nconf": "~0.6.9",
    "winston": "~0.7.2",
    "async": "~0.2.9",
    "mongodb": "~1.3.22",
    "ejs-locals": "~1.0.2",
    "connect-mongo": "~0.4.0",
    "validator": "~2.0.0",
    "string": "~1.7.0"
  }
}



Содержит в себе всю нужную информацию о проекте, а также все требуемые пакеты.

manage.js


var mongoose = require('./utils/mongoose'),
    async = require('async'),
    User = require('./models/user'),
    log = require('./utils/log')(null, module),
    config = require('./config');

function openConnection(cb) {
    mongoose.connection.on('open', function () {
        log.info('connected to database ' + config.get('db:name'));
        cb();
    });
}

function dropDatabase(cb) {
    var db = mongoose.connection.db;
    db.dropDatabase(function () {
        log.info('dropped database ' + config.get('db:name'));
        cb();
    });
}

function createBaseUser(cb) {
    var admin = new User({
        username: 'admin',
        password: config.get('project:admin:password'),
        email: config.get('project:admin:email'),
        role: 1
    });
    admin.save(function () {
        log.info('created database ' + config.get('db:name'));
        log.info('created base admin user');
        cb();
    });
}

function ensureIndexes(cb) {
    async.each(Object.keys(mongoose.models), function (model, callback) {
        mongoose.models[model].ensureIndexes(callback);
    }, function () {
        log.info('indexes ensured completely');
        cb();
    });
}

function closeConnection() {
    mongoose.disconnect();
    log.info('disconnected');
}

async.series(
    [
        openConnection,
        dropDatabase,

        createBaseUser,

        ensureIndexes
    ],
    closeConnection
);


Нужен для инициализации базы данных, заполнение default информацией, которой сервер будет оперировать.

config


config.json


{
	"port": 3000,
    "db": {
        "connection": "mongodb://localhost",
        "name": "db_name",
        "options": {
            "server": {
                "socketOptions": {
                    "keepAlive": 1
                }
            }
        }
    },
    "session": {
        "secret": "secret_key",
        "key": "cid",
        "cookie": {
            "path": "/",
            "httpOnly": true,
            "maxAge": null
        }
    }
}


index.js


var nconf = require('nconf');
var path = require('path');

nconf.argv()
    .env()
    .file({file: path.join(__dirname, 'config.json')});

module.exports = nconf;


В файле config.js содержится информация о настройках соединения с базой данных, а также настройки сессии.
Для работы с config используется пакет nconf, который позволяет через getter и setter манипулировать с объектом настроек. Также можно использовать вложенные объекты через символ ::

config.get('session:secret');
config.get('session:cookie:path');


middleware


module.exports = function (app, express) {
    var ejs = require('ejs-locals'),
        path = require('path'),
        config = require('../config'),

        mongoose = require('../utils/mongoose'),
        MongoStore = require('connect-mongo')(express),

        router = require('../routes'),
        errorHandler = require('./errorHandler')(app, express),

        checkAuth = require('./checkAuth');

    /**
     * Page Rendering
     * */
    app.engine('html', ejs);
    app.engine('ejs', ejs);
    app.set('views', path.join(__dirname, '../views'));
    app.set('view engine', 'ejs');


    /**
     * Favicon
     * */
    app.use(express.favicon('public/images/favicon.ico'));


    /**
     * Logger
     * */
    if (app.get('env') == 'development') {
        app.use(express.logger('dev'));
    }


    /**
     * Session
     * */
    app.use(express.bodyParser());
    app.use(express.cookieParser());
    app.use(express.session({
        secret: config.get('session:secret'),
        key: config.get('session:key'),
        cookie: config.get('session:cookie'),
        store: new MongoStore({mongoose_connection: mongoose.connection})
    }));

    /**
     * Authorization Access
     * */
    app.use(checkAuth);


    /**
     * Routing
     * */
    app.use(app.router);
    router(app);


    /**
     * Public directory
     * */
    app.use(express.static(path.join(__dirname, '../public')));
    app.use("/public", express.static(path.join(__dirname, '../public')));


    /**
     * Error handing
     * */
    app.use(errorHandler);
};


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

Хочу также отметить — errorHandler middleware предназначен для собственного handling ошибок сервера, и вывода страницы ошибки

errorHandler


var config = require('../config');

var sendHttpError = function (error, res) {
    res.status(error.status);

    if (res.req.xhr) {
        res.json(error);
    } else {
        res.render('error', {
            error: {
                status: error.status,
                message: error.message,
                stack: config.get('debug') ? error.stack : ''
            },
            project: config.get('project')
        });
    }
};

module.exports = function (app, express) {
    var log = require('../utils/log')(app, module),
        HttpError = require('../error').HttpError;

    return function(err, req, res, next) {
        if (typeof err === 'number') {
            err = new HttpError(err);
        }
        if (err instanceof HttpError) {
            sendHttpError(err, res);
        } else {
            if (app.get('env') === 'development') {
                express.errorHandler()(err, req, res, next);
            } else {
                log.error(err);
                err = new HttpError(500);
                sendHttpError(err, res);
            }
        }
    };
};


Также хочется отметить middleware checkAuth
var HttpError = require('../error').HttpError;

module.exports =  function (req, res, next) {
    if (!req.session.user) {
        return next(new HttpError(401, "You are not authorized!"));
    }
    next();
};

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

app.get('/user-info', checkAuth, function (req, res, next) {
    //do your staff
});


models


C помощью Mongoose мы будем создавать собственные модели для работы с данными. Пример модели может выглядеть следующим образом:

var crypto = require('crypto'),
    mongoose = require('../utils/mongoose'),
    Schema = mongoose.Schema,
    async = require('async');

var User = new Schema({
    username: {
        type: String,
        unique: true,
        required: true
    },
    hashedPassword: {
        type: String,
        required: true
    },
    salt: {
        type: String,
        required: true
    }
});

User.methods.encryptPassword = function (password) {
    return crypto.createHmac('sha1', this.salt).update(password).digest('hex');
};

User.virtual('password')
    .set(function (password) {
        this._plainPassword = password;
        this.salt = Math.random() + '';
        this.hashedPassword = this.encryptPassword(password);
    })
    .get(function () {
        return this._plainPassword;
    });

User.methods.checkPassword = function (password) {
    return this.encryptPassword(password) === this.hashedPassword;
};

module.exports = mongoose.model('User', User);



public


В данной директории будут содержаться все скрипты и css файлы, доступные извне. Осуществляется данная опция с помощью следующей настройки:

/**
 * Public directory
 * */
app.use(express.static(path.join(__dirname, '../public')));
app.use("/public", express.static(path.join(__dirname, '../public')));


routes


Cамое, пожалуй, интересное. В данной директории, мы объявляем модуль, который будет отвечает за роутинг. файл index.js

var main = require('./main'),
    register = require('./register'),
    authentication = require('./authentication'),
    error = require('./error');

module.exports = function (app) {
    app.get('/', main.home);

    app.post('/register', register.requestRegistration);

    app.get('/users', authentication.users);
    app.get('/users/:id', authentication.user);

    app.get('*', error['404']);
};


Здесь мы просто объявляем наши роуты, и просто делегируем выполенение другим модулям. Например, route "/":
/**
 * Method: GET
 * URI: /
 * */
exports.home = function(req, res, next) {
    res.render('index');
};


Cобственно говоря и все. В данном случае, как база приложение будет работать. Для поддержки сессии включаем соответствующий middleware. Всю бизнес логику, связанную с пользователем, переносим в models/user.js, в частности валидацию и регистрацию, к примеру.

PS:
В написании данной статьи была использована информация из скринкастов И.Кантора. Ссылка на скринкаст.
Также использовалась информация из курсов по MongoDB
Share post

Similar posts

Comments 47

    +2
    Чувствую влияние скринкастов по NodeJS от Ильи Кантора =)
      0
      Вот хорошее демо github.com/madhums/node-express-mongoose-demo как раз примерно ваш набор модулей.
        +1
        А, кстати, зачем вам модуль mongodb, у вас же mongoose…
          0
          Часто бывают случаи, когда нужно обращаться к БД напрямую.
            0
            Ну так у вас же есть mongoose.connection.db. Именно так автор и делает, например, dropDatabase.
              0
              Совершенно верно, теоретически еще можно получить доступ через mongoose connection, но, оно поддерживает лишь часть нативных возможностей
                0
                Что нельзя сделать из mongoose?
                  0
                  в принципе, если приглядеться — то можно увидеть в зависимостях mongoose нативный драйвер mongodb:
                  image

                  и фактически да, через mongoose можно его использовать, таким образом выполнять все операции.
            +5
            Пара моментов:
            1. middleware.registerMiddleware(app, express) — как масло масляное. require('./middleware')(app, express) — в принципе самое то.
            2.
            if (res.req.headers['x-requested-width'] === 'XMLHttpRequest')
            -->
            if (req.xhr)
              0
              спасибо согласен, так лучше будет
              +1
              А чего все так прутся от winston?
                0
                просто дает возможности для «продвинутого» логирования, также подсветка разных уровней логов. На самом деле можно использовать хоть собственную обвертку над console.log, тут кому как угодно.
                  0
                  Если не сложно поясните что вы подразумеваете под «продвинутым» логированием?
                  Просто я не понимаю, какой понт в 10 уровнях логирования и подстветке логов как основных фич, логи используются для анализа проблемы после того как она случилась и ни одна ни другая фича в этом не помогут. А так да красивые сообщения в консоли — это мило, только бесполезно.
                  Еще заметил, что как то очень мало внимаю уделяется форматированию сообщений (н-р в большинстве либ нету миллисекунд, так как используется Date#toString).
                    0
                    Фактически что вы описали, но я не задумывался о других модулях логирования, из комментария ниже можно попробовать пакет intel. Сейчас его попробую. Быть может Вы и правы.
                      0
                      У winston можно задавать дополнительные транспорты, то есть писать логи не только в консоль, но и в файл, в бд, на какой-нибудь удаленный сервис и тд. В купе с уровнями логирования и NODE_ENV может получиться «продвинутое логирование».
                        0
                        Полагаю тут на любителя (комбайн — либа). Я за unix-way — пусть логгер логирует, а куда я сам разберусь средствами ОС (есть ведь из чего выбирать). Потому как не легко написать грамотный логгер, который не будет жрать много ресурсов приложения и адекватно себя вести во всех ситуациях. Сами понимаете, что логи ценны именно тогда, когда само приложение уже может быть не в адекватном состоянии, не следует добивать его логгером.
                          0
                          Да, я, если честно, думал применить в своем проекте транспорты winston'а на продашне, но, в итоге, обошелся простым перенаправлением stdout.

                          Думаю, такая популярность winston возникла из-за курса Ильи Кантора.

                            0
                            Скорее всего, но как по мне, один раз настроил как тебе нужно — и пользуйся.
                              0
                              Можно подробнее про перенаправление stdout? Т.е. console.log писал в файл?
                                +1
                                Да. Я написал маленький логгер, чтобы все логи выглядели единообразно, ну и:
                                node_app >> ./app.logs
                              0
                              Сам пускаю ноду через upstart так стандартный console.log, естественно, замечательно в лог попадает. Но access.log еще же надо.

                              Тут бы с радостью принял чтото совсем простое — и хорошобы вообще без зависимостей.
                                0
                                Если хочется, совсем без зависимостей, то можно как то так попробовать:
                                $ node
                                > var Console = require('console').Console
                                > var fs = require('fs')
                                > var fileStream = fs.createWriteStream('./access.log');
                                > var access = new Console(fileStream, fileStream);
                                > access.log('GET /aaa.txt 200')
                                > 
                                ^C
                                $ cat access.log 
                                GET /aaa.txt 200
                                


                                Я лично использую слегка дополненный connect logger, чтобы просто направлять от него сообщения в отдельный логгер.
                                  0
                                  В реп положилбы — ленивых много :D
                            0
                            **cообщение удалено**
                            0
                            Log4js подойдет?
                              0
                              Он как минимум медленный.
                                0
                                Сделай статью с бенчами логгеров. У тебя почти все есть уже для этого.
                                  0
                                  Ок, сегодня — завтра попробую
                            0
                            Я тоже не понимаю почему все от него прутся, крайне запутанное апи и плохая настраиваемость, по крайней мере я так и не смог его нормально использовать, особенно раздражало что нельзя иметь именнованые логеры и нормальные фильтры. Всем кто прется от винстона рекомендую посмотреть в сторону Intel github.com/seanmonstar/intel
                            ну или ваш форк =) github.com/btd/rufus
                              0
                              Спасибо, сейчас посмотрим =)
                            0
                            app.use(express.static(...)); в хвост, иначе на каждый запрос по диску елозит. А с fstat все не очень хорошо — github.com/joyent/node/issues/6662
                              0
                              Опустил. Но в случае если нужно отрендерить единственную страницу с public папки, то эту опцию нужно оставить выше router, в случае если в основном использовать сервер как backend.
                                0
                                Если app.use('/public', express.static(...)); то без разницы.
                                Разница есть только в случае если у вас есть какойто «контроллер» будет за '/public'.
                              0
                              вот очень хорошая книжка на эту тему
                                0
                                Недавно прочитал ее. В принципе там не много полезной информации, очень много воды, хотя может для новичков самое оно.
                                  0
                                  Как бы и статья, вроде, для новичков. Если в «Learnning Node» вы нашли много воды, то вы, наверное, уже выросли из книжек, попробуйте почитать исходники каких-нибудь фреймворков
                                0
                                еще на эту тему можно посмотреть krakenjs.com
                                  +1
                                  Удивляет, почему Mongoose, Sequelize и прочие назвают моделью то, что по всем признакам выглядит как репозиторий, которому при инстанцировании как раз таки скармливается именно модель; и который на операции поиска выплевывает именно инстансы моделей. В том числе сохраняет и удаляет модели именно репозиторий, насколько я себе представляю смысл этого паттерна.
                                  www.codeproject.com/Articles/644605/CRUD-Operations-Using-the-Repository-Pattern-in-MV

                                  Греховность этой терминологической каши у разных ORM под Node.js состоит в том, что люди навешивают кастомные методы вроде бы как на модель, но по факту они навешиваются на репозиторий, который потом прикручивает эти самые методы собранным моделям. Все это порождает путаницу, например, в разруливании задачи: создать кастомный метод репозитория (замысловатую выборку, к примеру). И как я вижу решают люди эту проблему частенько саммым нездоровым способом – raw SQL прямо в теле контроллера. Это вместо того, чтобы расширить репозиторий.

                                  Кстати, утверждение, что вся логика должна сидеть в моделях – спорное. Ребята из Зенда, например, так не думают и я склонен с ними согласиться. www.slideshare.net/weierophinney/architecting-your-models

                                  Вообще заметна склонность у авторов замечательных библиотек под javascript вносить, скажем так, сумятицу в термины, имеющие доселе конкретное понимание (вспомним Backbone, которые заварили кашу с «Controller was renamed to Router, for clarity» backbonejs.org/, вспомним oAuth2orize, который методом «var server = oauth2orize.createServer();» создает вовсе не сервер, а медиатр (middleware) github.com/jaredhanson/oauth2orize). Странные они.
                                    0
                                    Вот мой пример организации кода и написания небольшого приложения на express на примере очень простого движка для блога «node-blog».
                                      0
                                      А почему mongoose.js файл пустой? Или это модуль? Про этот файл ничего не сказано…
                                        0
                                        var mongoose = require('mongoose');
                                        var config = require('../config');
                                        
                                        mongoose.connect(
                                            config.get('db:connection') + '/' + config.get('db:name'),
                                            config.get('db:options')
                                        );
                                        module.exports = mongoose;
                                        
                                        


                                        это просто модуль с уже сохраненным объектом mongoose, который подключен к базе.
                                        0
                                        Не нашел в статье сырцов /routes/error.js
                                        Очень смущает app.get('*', error['404']); — у меня такой роутинг перехватывает все запросы подряд, несмотря на то что он указан последним.
                                        Было бы неплохо выложить рабочий вариант этой аппликухи на гитхаб, чтобы вживую посмотреть работу предложенной структуры приложения. Это возможно?
                                          0
                                          errorHandler:

                                          var config = require('../config');
                                          
                                          var sendHttpError = function (error, res) {
                                              res.status(error.status);
                                          
                                              if (req.xhr) {
                                                 // ....
                                              }
                                          


                                          Подскажите плз «req» (… if (req.xhr) ...) где объявляется?
                                            0
                                            тут поправочку нужно сделать:

                                            ...
                                             if (res.req.xhr) {
                                                    res.json(error);
                                                } else {
                                            ...
                                            
                                            0
                                            Очень смущает app.get('*', error['404']); — у меня такой роутинг перехватывает все запросы подряд, несмотря на то что он указан последним.


                                            Это очень странно. Возможно, отправляя ответ клиенту, вы вызываете `next()` в обработчике? Можете показать пример, который к этому приводит?
                                              0
                                              Прошу прощения, вопрос снят — был баг в реализации контроллера обработки ошибок.
                                              0
                                              Этот роут опустите в самый низ. Он будет перехватывать оставшиеся не зарегистрированные маршруты и выдавать ошибку

                                            Only users with full accounts can post comments. Log in, please.