Skip to main content

Express

Folder Structure

project-root/
├── src/
│ ├── api/
│ │ ├── controllers/
│ │ │ ├── userController.js
│ │ │ ├── productController.js
│ │ │ └── ...
│ │ ├── routes/
│ │ │ ├── userRoutes.js
│ │ │ ├── productRoutes.js
│ │ │ └── index.js
│ │ ├── middleware/
│ │ │ ├── authMiddleware.js
│ │ │ ├── errorMiddleware.js
│ │ │ └── ...
│ │ └── validators/
│ │ ├── userValidator.js
│ │ ├── productValidator.js
│ │ └── ...
│ ├── config/
│ │ ├── database.js
│ │ └── ...
│ ├── models/
│ │ ├── userModel.js
│ │ ├── productModel.js
│ │ └── ...
│ ├── services/
│ │ ├── userService.js
│ │ ├── productService.js
│ │ └── ...
│ ├── utils/
│ │ ├── logger.js
│ │ ├── errorHandler.js
│ │ └── ...
│ └── app.js
├── tests/
│ ├── unit/
│ │ ├── userService.test.js
│ │ └── ...
│ ├── integration/
│ │ ├── userRoutes.test.js
│ │ └── ...
│ └── setup.js
├── .env
├── .gitignore
├── package.json
├── README.md
└── server.js

Key Components and Best Practices

Root Files

  • server.js: Entry point for the application. Initializes the Express app, connects to the database, and starts the server.

    import express from "express";
    import "dotenv/config.js";
    import { connectDB } from "./src/config/database.js";
    import routes from "./src/api/routes/index.js";

    const app = express();
    app.use(express.json());
    app.use("/api", routes);

    const PORT = process.env.PORT || 3000;
    connectDB().then(() => {
    app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
    });
  • .env: Stores environment variables (e.g., PORT, DATABASE_URL).

  • .gitignore: Excludes node_modules, .env, and build artifacts.

  • package.json: Defines dependencies and scripts. Ensure "type": "module" is set for ES6 modules.

    {
    "type": "module",
    "scripts": {
    "start": "node server.js",
    "dev": "nodemon server.js",
    "test": "jest"
    }
    }
  • README.md: Documents setup, usage, and API endpoints.

src

  • app.js: Configures the Express app (middleware, global settings) but doesn’t start the server.

    import express from "express";
    import helmet from "helmet";
    import cors from "cors";
    import morgan from "morgan";
    import errorMiddleware from "./api/middleware/errorMiddleware.js";

    const app = express();
    app.use(helmet());
    app.use(cors());
    app.use(morgan("dev"));
    app.use(express.json());
    app.use(errorMiddleware);

    export default app;

api

  • controllers/: Handles request/response logic for each endpoint.

    userController.js
    import * as userService from "../services/userService.js";

    export const getUsers = async (req, res, next) => {
    try {
    const users = await userService.getAllUsers();
    res.status(200).json(users);
    } catch (error) {
    next(error);
    }
    };
  • routes/: Defines API routes and maps them to controllers.

    userRoutes.js
    import { Router } from "express";
    import * as userController from "../controllers/userController.js";
    import { validateUser } from "../validators/userValidator.js";

    const router = Router();
    router.get("/", userController.getUsers);
    router.post("/", validateUser, userController.createUser);

    export default router;
    index.js
    import { Router } from "express";
    import userRoutes from "./userRoutes.js";
    import productRoutes from "./productRoutes.js";

    const router = Router();
    router.use("/users", userRoutes);
    router.use("/products", productRoutes);

    export default router;
  • middleware/: Contains reusable middleware (e.g., authentication, error handling).

    authMiddleware.js
    import jwt from "jsonwebtoken";

    export const authenticate = (req, res, next) => {
    const token = req.headers.authorization?.split(" ")[1];
    if (!token) return res.status(401).json({ message: "No token provided" });

    jwt.verify(token, process.env.JWT_SECRET, (err, decoded) => {
    if (err) return res.status(401).json({ message: "Invalid token" });
    req.user = decoded;
    next();
    });
    };
  • validators/: Validates incoming request data (e.g., using Joi or Zod).

    userValidator.js
    import Joi from "joi";

    export const validateUser = (req, res, next) => {
    const schema = Joi.object({
    name: Joi.string().required(),
    email: Joi.string().email().required(),
    });
    const { error } = schema.validate(req.body);
    if (error)
    return res.status(400).json({ message: error.details[0].message });
    next();
    };

config

  • database.js: Manages database connection (e.g., MongoDB, PostgreSQL).

    import mongoose from "mongoose";

    export const connectDB = async () => {
    try {
    await mongoose.connect(process.env.DATABASE_URL);
    console.log("Database connected");
    } catch (error) {
    console.error("Database connection error:", error);
    process.exit(1);
    }
    };

models

  • Defines database schemas/models (e.g., for Mongoose or Sequelize).

    userModel.js
    import mongoose from "mongoose";

    const userSchema = new mongoose.Schema({
    name: { type: String, required: true },
    email: { type: String, required: true, unique: true },
    });

    export default mongoose.model("User", userSchema);

services

  • Contains business logic, interacting with models and external APIs.

    userService.js
    import User from "../models/userModel.js";

    export const getAllUsers = async () => {
    return await User.find();
    };

    export const createUser = async (data) => {
    return await User.create(data);
    };

utils

  • Stores reusable utilities (e.g., logging, error handling).

    logger.js
    import winston from "winston";

    const logger = winston.createLogger({
    level: "info",
    format: winston.format.json(),
    transports: [new winston.transports.File({ filename: "logs/app.log" })],
    });

    export default logger;

tests

  • Organizes unit and integration tests using a framework like Jest.

    userService.test.js
    import { getAllUsers } from "../src/services/userService.js";
    import User from "../src/models/userModel.js";

    jest.mock("../src/models/userModel.js");

    describe("User Service", () => {
    it("should get all users", async () => {
    User.find.mockResolvedValue([{ name: "Test" }]);
    const users = await getAllUsers();
    expect(users).toEqual([{ name: "Test" }]);
    });
    });

Example for Larger Projects

For very large projects, you can modularize further. This groups all user-related files together, improving modularity.

src/
├── modules/
│ ├── user/
│ │ ├── userController.js
│ │ ├── userRoutes.js
│ │ ├── userService.js
│ │ ├── userModel.js
│ │ └── userValidator.js
│ ├── product/
│ │ ├── productController.js
│ │ └── ...
│ └── ...
├── common/
│ ├── middleware/
│ ├── utils/
│ └── ...
├── config/
├── app.js
└── server.js

Example for MVC Project

project-root/
├── src/
│ ├── config/
│ │ ├── database.js
│ │ ├── env.js
│ │ └── ...
│ ├── controllers/
│ │ ├── userController.js
│ │ ├── productController.js
│ │ └── ...
│ ├── models/
│ │ ├── userModel.js
│ │ ├── productModel.js
│ │ └── ...
│ ├── views/
│ │ ├── user/
│ │ │ ├── index.ejs
│ │ │ ├── create.ejs
│ │ │ └── ...
│ │ ├── product/
│ │ │ ├── index.ejs
│ │ │ └── ...
│ │ ├── partials/
│ │ │ ├── header.ejs
│ │ │ ├── footer.ejs
│ │ │ └── ...
│ │ └── error.ejs
│ ├── routes/
│ │ ├── userRoutes.js
│ │ ├── productRoutes.js
│ │ └── index.js
│ ├── middleware/
│ │ ├── authMiddleware.js
│ │ ├── errorMiddleware.js
│ │ └── ...
│ ├── services/
│ │ ├── userService.js
│ │ ├── productService.js
│ │ └── ...
│ ├── utils/
│ │ ├── logger.js
│ │ ├── errorHandler.js
│ │ └── ...
│ ├── public/
│ │ ├── css/
│ │ │ ├── styles.css
│ │ │ └── ...
│ │ ├── js/
│ │ │ ├── scripts.js
│ │ │ └── ...
│ │ └── images/
│ └── app.js
├── ...

Update for important files

server.js
import express from "express";
import { connectDB } from "./src/config/database.js";
import routes from "./src/routes/index.js";
import path from "path";
import app from "./src/app.js";

// Focus here!
app.set("views", path.join(process.cwd(), "src/views"));
app.set("view engine", "ejs");
app.use(express.static(path.join(process.cwd(), "src/public")));
app.use("/api", routes);

const PORT = process.env.PORT || 3000;
connectDB().then(() => {
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
});
app.js
app.use(express.urlencoded({ extended: true })); // For form submissions

For views

index.ejs
<!-- Can use pug, hsb, ... -->
<%- include('../partials/header') %>
<h1>Users</h1>
<ul>
<% users.forEach(user => { %>
<li><%= user.name %> (<%= user.email %>)</li>
<% }) %>
</ul>
<a href="/users/create">Add User</a>
<%- include('../partials/footer') %>
userController.js
import * as userService from "../services/userService.js";

export const getUsers = async (req, res, next) => {
try {
const users = await userService.getAllUsers();
res.render("user/index", { users }); // Render view
} catch (error) {
next(error);
}
};

// get -> /users/create
export const getCreateUserForm = (req, res) => {
res.render("user/create"); // Render create form
};

// post -> users
export const createUser = async (req, res, next) => {
try {
await userService.createUser(req.body);
res.redirect("/users"); // Redirect after form submission
} catch (error) {
next(error);
}
};

References