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.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);
}
};