Best practices for ExpressJS (Part I)

Tổng quan:

Express không có sẵn và bắt buộc cấu trúc thư mục chặt chẽ. Thay vào đó chúng ta có thể build ứng dụng theo cách mà chúng ta muốn. Điều này thực được rất tuyệt vời đặc biệt là đối với ứng dụng nhỏ. Nó dễ dàng để bắt đầu, học tập và trải nghiệm.

Tuy nhiên, khi ứng dụng lớn và phức tạp hơn, nhiều thứ có thể gây sự lộn xộn và bối rối. Một ứng dụng tốt phải đáp ứng được các điểm chính sau:

  • Cấu trúc rõ ràng, dễ hiểu, dễ phát triển (clean code)
  • Dễ dàng đáp ứng sự thay đổi và mở rộng (scalable)
  • Maintain đơn giản, nhanh chóng và thuận tiện (maintainable)

Như vậy cấu trúc project chính là linh hồn của dự án, nó quyết định chính đến chất lương, tốc độ của dự án. Như chúng ta đã biết, thời gian fix bug, thay đổi và maintain chiếm 70% khối lượng công việc. Dự án thành công ngoài việc làm đúng bussiness logic thì phải giảm thiếu được 70% về mức min nhất. Tức luôn tính đến khả năng mở rộng, bảo trì. Điều này cực kì quan trọng.

Phần lớn lập trình viên mới, họ nhanh chóng bắt tay vào code chức năng chính luôn mà ít quan tâm tới việc phân tích, định hướng cấu trúc dự án. Với suy nghĩ càng nhanh càng tốt, chức năng đầu tiên hoàn thành nhanh càng tốt. Nhưng dục tốc thì bất đạt. Vì rất nhanh chóng tầm 1, 2 tuần sau. tốc độ phát triển bị chậm lại và đặc biệt khi có sự thay đổi từ phía khách hàng thì code trở nên lộn xộn, messy, và phức tạp. Kéo theo thời gian phát triển maintain bị đội lên, càng ngày code càng rối rắm như một vòng xoáy. Bản thân mình cũng đã từng trải nghiệm gỡ rối những dự án như thế này. Dự án cần đáp ứng schedule về mặt thời gian, tức cần nhanh. Nhưng từ nhanh ở đây là cả quá trình phát triển và maintain dự án chứ không phải nhanh ở tính năng.

Cấu trúc thư mục

ExpressJs không tạo ra một cấu trúc chuẩn nào để tạo nên sự linh hoạt đơn giản. Nên chúng ta sẽ tự cấu trúc theo cách của mình. Design partern MVC kết hợp với cấu trúc thư mục theo chức năng là một sự lựa chọn hòan hảo. Nó đơn giản, dễ hiểu và đáp ứng được 3 nguyên tắc trên. Cấu trúc như sau:

project/
    server
        controllers/
            comments.js
            index.js
            users.js
        helpers/
            dates.js
        middlewares/
            auth.js
            users.js
        models/
            comment.js
            user.js
        migrations/
        routes/
            index.js
        public/
            js/
            css/
            img/
        views/
            comments/
                comment.jade
            users/
                index.jade
        tests/
        app.js
    .gitignore
    package.json
  • controllers/: thư mục chứa controllers
  • helpers/: thư mục chứa các chức năng, thành phần chung, chia sẻ cho nhiều phần khác nhau của dự án.
  • middlewares/: thư mục chứa các middlewares
  • migrations/: thư mục chứa các file migrations
  • models/: thư mục chứa xử lý với cơ sở dữ liệu
  • routes/: thư mục chứa route
  • public/: thư mục chứa các file tĩnh như image, css, javascript
  • views/: thư mục chứa các template, view
  • tests/: thư mực chứa code viết Unit test
  • app.js : file chính, khởi tạo dự án
  • package.json: file config và chứa các module node đã cài đặt

Ví dụ

Chúng ta sẽ tạo ra một ứng dụng demo có sử dụng migrations.

Yêu cầu

Trước khi bắt đầu chúng ta cần cài đặt và có hiểu biết cớ bản về các nền tảng, package và tool sau:

  • NodeJS
  • Express
  • PostgreSQL
  • Sequelize - ORM database
  • Postman - A Chrome app để test API

Khởi tạo project

Chay lệnh sau:

$ mkdir -p nodejs-demo/{bin,server}
$ cd nodejs-demo

Khởi tạo npm để tạo file package.json

$ npm init -y

Thư mục dự án bây giờ trông như thé này:

nodejs-demo
├── bin
├── package.json
└── server

Cài đặt expessjs và một số dependencies

$ npm install --save express body-parser morgan

Cờ --save sẽ lưu package vào phần dependencies của file package.json Tạo 1 file ở thư mục root gọi là app.js.

$ touch app.js

Trong file này, chúng ta sẽ tạo ứng dụng Express có nội dung như sau:

const express = require('express');
const logger = require('morgan');
const bodyParser = require('body-parser');

// Set up the express app
const app = express();

// Log requests to the console.
app.use(logger('dev'));

// Parse incoming requests data (https://github.com/expressjs/body-parser)
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));

// Setup a default catch-all route that sends back a welcome message in JSON format.
app.get('*', (req, res) => res.status(200).send({
  message: 'Welcome to the beginning of nothingness.',
}));

module.exports = app;

Trong thư mục bin, tạo 1 file gọi là www

$ touch bin/www

Tạo nội dung file www như sau:

// This will be our application entry. We'll setup our server here.
const http = require('http');
const app = require('../app'); // The express app we just created

const port = parseInt(process.env.PORT, 10) || 8000;
app.set('port', port);

const server = http.createServer(app);
server.listen(port);

Để có thể restart server mỗi khi code có sự thay đổi thì chúng ta dùng package nodemon.

$ npm install  --save  nodemon

Sau đó, mở file package.json, tạo một command để run server. Command này được trong trong phần scripts của file package.json. Nội dung như sau:

...
"scripts": {
  "start:dev": "nodemon ./bin/www",
  "test": "echo \"Error: no test specified\" && exit 1"
},
...

Bây giờ chạy ứng dụng bằng command

$ npm run start:dev

Truy cập http://localhost:8000. Chúng ta thấy message như thế này {"message":"Welcome to the beginning of nothingness."}.

Tới đây, thư mục của chúng ta sẽ như thé này:

nodejs-demo
├── app.js
├── bin
│   └── www
├── package.json
└── server

Cài đặt Sequelize

Tiếp đến chúng ta sẽ require Sequelize. Nó là một ORM database. Nó còn cung cấp chức năng database migrations giúp chúng ta quản lý database cực tốt. Để tìm hiểu chi tiết chúng ta truy cập vào trang http://docs.sequelizejs.com/manual/installation/getting-started.html

Chúng ta sẽ cái đặt package Sequelize CLI.

$ npm install -g sequelize-cli

Tham số -g ở đây nghĩa là cài đặt global cho package này. Tiếp đến chúng ta config Sequelize cho dự án. Đầu tiên tạo file .sequelizerc trong thư mục root của dự án. Trong file này chúng ta sẽ chỉ rõ đường dẫn (path) được require bởi Sequelize:

const path = require('path');

module.exports = {
  "config": path.resolve('./server/config', 'config.json'),
  "models-path": path.resolve('./server/models'),
  "seeders-path": path.resolve('./server/seeders'),
  "migrations-path": path.resolve('./server/migrations')
};

File config.json chứa cài đặt của toàn bộ ứng dụng như database authentication config, migrations folder. Trong khi đó thư mục models sẽ chứa tất cả model của ứng dụng. Chúng ta cần cài đặt package Sequelize như một dependencies.

$ npm install --save sequelize pg pg-hstore

pg sẽ chịu trách nhiệm tạo database connection trong khi đó pg-hstore là một module để serializingdeserializing JSON dâta vào Postgres hstore format. Bây giờ với các đường dẫn (path) đã định nghĩa. chúng ta sẽ chạy command để khởi tạo các thư mục đã config và code mẫu.

$ sequelize init

Cấu trúc dự án giờ trông như thé này:

nodejs-demo
├── app.js
├── bin
│   └── www
├── package.json
└── server
    ├── config
    │   └── config.json
    ├── migrations
    ├── models
    │   └── index.js
    └── seeders

Chúng ta có thể thấy file server/models/index.js đã được tạo tự động. Nội dung như sau:

'use strict';

var fs        = require('fs');
var path      = require('path');
var Sequelize = require('sequelize');
var basename  = path.basename(module.filename);
var env       = process.env.NODE_ENV || 'development';
var config    = require(__dirname + '/../config/config.json')[env];
var db        = {};

if (config.use_env_variable) {
  var sequelize = new Sequelize(process.env[config.use_env_variable]);
} else {
  var sequelize = new Sequelize(config.database, config.username, config.password, config);
}

fs
  .readdirSync(__dirname)
  .filter(function(file) {
    return (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js');
  })
  .forEach(function(file) {
    var model = sequelize['import'](path.join(__dirname, file));
    db[model.name] = model;
  });

Object.keys(db).forEach(function(modelName) {
  if (db[modelName].associate) {
    db[modelName].associate(db);
  }
});

db.sequelize = sequelize;
db.Sequelize = Sequelize;

module.exports = db;

Trong file này, các module cần thiết để tương tác với database đã được sử dụng. Đọc config Node environment. Nếu không được chỉ định thì lấy mặc định (default) là development. Sau đó là thiết lập một kết nối tới database. Tiếp đến là đọc thư mục models, import tất cả các models, thêm vào đối tượng db và áp dụng relationship giữa các models. Tương đối dễ hiểu phải không nào?

Chỉnh sửa lại file server/models/index.js. Code mặc định sinh ra file này là ES5 syntax. Chúng ta sửa lại sử dụng ES6 syntax. Nếu chưa biết ES6 các bạn có thể tim hiểu thêm trên mạng. File server/models/index.js như sau:

const fs = require('fs');
const path = require('path');
const Sequelize = require('sequelize');
const basename = path.basename(module.filename);
const env = process.env.NODE_ENV || 'development';
const config = require(`${__dirname}/../config/config.json`)[env];
const db = {};

let sequelize;
if (config.use_env_variable) {
  sequelize = new Sequelize(process.env[config.use_env_variable]);
} else {
  sequelize = new Sequelize(
    config.database, config.username, config.password, config
  );
}

fs
  .readdirSync(__dirname)
  .filter(file =>
    (file.indexOf('.') !== 0) &&
    (file !== basename) &&
    (file.slice(-3) === '.js'))
  .forEach(file => {
    const model = sequelize.import(path.join(__dirname, file));
    db[model.name] = model;
  });

Object.keys(db).forEach(modelName => {
  if (db[modelName].associate) {
    db[modelName].associate(db);
  }
});

db.sequelize = sequelize;
db.Sequelize = Sequelize;

module.exports = db;

Đâu tiên, chúng ta cần tạo một development database. Giả sử ta đã có một tài khoản postgresqlsammy và mật khẩu là 123456. Đăng nhập vào tài khoản này và tạo db todos-dev.

$ sudo -su sammy psql 
$ createdb todos-dev

createdb command có thể được sử dụng sau khi cài đặt PostgreSQL.

Cập nhật lại file `config.json` như sau:
{
  "development": {
    "username": "sammy",
    "password": 123456,
    "database": "todos-dev",
    "host": "127.0.0.1",
    "port": 5432,
    "dialect": "postgres"
  },
  "test": {
    "username": "sammy",
    "password": 123456,
    "database": "todos-test",
    "host": "127.0.0.1",
    "port": 5432,
    "dialect": "postgres"
  }
}

Chúng ta nên tạo ra các thư mục cần thiết khác tùy ý trong thư mục server.

$ cd server 
$ mkdir controllers middlewares libs helpers routes

Bài viết khá dài nên mình sẽ dừng phần 1 tại đây. Sang phần 2 mình sẽ hướng dẫn các bạn viết controller, model, migration cho feature todo tasks. Các bạn có thể download code tại đây

Tham khảo