Lesson 41-Express Project Architecture Design

Directory Structure

A typical Express project directory structure is as follows:

project-name/
|-- node_modules/
|-- public/
|   |-- css/
|   |-- js/
|   |-- images/
|-- views/
|-- routes/
|-- controllers/
|-- models/
|-- middleware/
|-- config/
|-- tests/
|-- .gitignore
|-- package.json
|-- package-lock.json
|-- README.md
|-- app.js
|-- server.js
  • public/: Stores static assets such as CSS, JavaScript, and images.
  • views/: Stores view template files.
  • routes/: Stores route modules.
  • controllers/: Stores controller logic for handling business logic and data operations.
  • models/: Stores data models, such as model definitions for ORMs like Sequelize or Mongoose.
  • middleware/: Stores middleware modules.
  • config/: Stores configuration files, such as database connections and environment variables.
  • tests/: Stores test files.
  • .gitignore: Lists files and directories to be ignored by Git.
  • package.json: Specifies project dependencies and scripts.
  • app.js: The entry file for the Express application.
  • server.js: The main file for starting the server, loading configurations, and middleware.
  • README.md: Project documentation.

Module Division

Modularity is a core principle of modern JavaScript development. Dividing code into small, reusable modules enhances readability and maintainability.

  • Route Modules: Each route module handles a set of related APIs or pages.
  • Controller Modules: Each controller module manages a set of related business logic.
  • Model Modules: Each model module represents a table or collection in the database.

Dependency Management

Use npm or yarn to manage project dependencies. List all dependencies and their versions explicitly in the package.json file.

{
    "dependencies": {
        "express": "^4.17.1",
        "mongoose": "^6.0.14",
        "body-parser": "^1.19.0"
    },
    "devDependencies": {
        "nodemon": "^2.0.13",
        "mocha": "^9.1.3",
        "chai": "^4.3.4"
    }
}

Startup Scripts

Define startup and test scripts in package.json to streamline the development process.

"scripts": {
    "start": "node server.js",
    "dev": "nodemon server.js",
    "test": "mocha --recursive tests/"
}

Configuration Files

Store environment variables and sensitive information in a .env file, and use the dotenv package to load these variables.

// config/env.js
require('dotenv').config();
module.exports = {
    port: process.env.PORT || 3000,
    db: {
        host: process.env.DB_HOST,
        user: process.env.DB_USER,
        password: process.env.DB_PASSWORD,
        database: process.env.DB_NAME
    }
};

Routes and Controllers

// routes/users.js
const express = require('express');
const router = express.Router();
const UserController = require('../controllers/UserController');

router.get('/', UserController.index);
router.post('/', UserController.create);

module.exports = router;
// controllers/UserController.js
const UserService = require('../services/UserService');

exports.index = (req, res) => {
    UserService.getAllUsers()
        .then(users => res.json(users))
        .catch(err => res.status(500).json({ error: err.message }));
};

exports.create = (req, res) => {
    const newUser = req.body;
    UserService.createUser(newUser)
        .then(user => res.status(201).json(user))
        .catch(err => res.status(400).json({ error: err.message }));
};

Data Models

If using MongoDB and Mongoose, a model definition might look like this:

// models/User.js
const mongoose = require('mongoose');
const Schema = mongoose.Schema;

const UserSchema = new Schema({
    username: { type: String, required: true, unique: true },
    email: { type: String, required: true, unique: true },
    password: { type: String, required: true }
}, { timestamps: true });

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

Middleware

Middleware handles tasks shared across multiple routes, such as logging, error handling, and authentication.

// middleware/logger.js
const morgan = require('morgan');

module.exports = morgan('combined');

Testing

Write unit and integration tests to ensure code quality and stability.

// tests/controllers/UserController.test.js
const chai = require('chai');
const sinon = require('sinon');
const sinonChai = require('sinon-chai');
const UserService = require('../../services/UserService');
const UserController = require('../../controllers/UserController');

chai.use(sinonChai);
const expect = chai.expect;

describe('UserController', () => {
    it('should return all users', async () => {
        const req = {};
        const res = {};
        sinon.stub(UserService, 'getAllUsers').resolves([{ id: 1 }, { id: 2 }]);
        res.json = sinon.spy();
        await UserController.index(req, res);
        expect(res.json).to.have.been.calledWith([{ id: 1 }, { id: 2 }]);
        UserService.getAllUsers.restore();
    });
});
Share your love