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: Excludesnode_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.jsimport * 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.jsimport { Router } from "express";import * as userController from "../controllers/userController.js";import { validateUser } from "../validators/userValidator.js";const router = Router();router.get("/", userController.getUsers);router.get("/:id", userController.getUserDetails);router.post("/", validateUser, userController.createUser);export default router;index.jsimport { 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.jsimport 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.jsimport 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.jsimport 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.jsimport 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.jsimport 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.jsimport { 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
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.use(express.urlencoded({ extended: true })); // For form submissions
For views
<!-- 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') %>
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);
}
};
Tips
Seeding Database
import mongoose from "mongoose";
const userSchema = new mongoose.Schema({
name: String,
email: String,
});
const User = mongoose.model("User", userSchema);
// Seed sample data if collection is empty
User.countDocuments().then((count) => {
if (count === 0) {
User.insertMany([
{ name: "Alice", email: "alice@example.com" },
{ name: "Bob", email: "bob@example.com" },
]).then(() => console.log("Sample users added"));
}
});
export default User;
Useful Packages
| Layer | Package | Role in Project |
|---|---|---|
| Middleware & Security | helmet | Sets security-related HTTP headers |
| cors | Enables cross-origin access to your server | |
| morgan | Logs HTTP requests for debugging | |
| express-session | Manages user sessions | |
| multer | Handles file uploads from forms | |
| Configuration | dotenv | Loads environment variables from .env file |
| Authentication & Authorization | jsonwebtoken | Generates and verifies JWT tokens |
| Validation | express-validator | Validates and sanitizes incoming requests |
| Database Layer | mongoose | Object Data Modeling (ODM) for MongoDB |