backend-express-generator
v1.0.1
Published
A production-ready Node.js CLI tool that scaffolds a professional Express.js + MongoDB boilerplate.
Maintainers
Readme
backend-express-generator
A production-ready Express.js + MongoDB REST API scaffolder — batteries included.
# Global install (Recommended)
npm i -g backend-express-generator
express-gen my-api
# Or using npx
npx backend-express-generator my-apiTable of Contents
- Quick Start
- Folder Structure — What Goes Where?
- Step 1 — Setting Up MongoDB
- Step 2 — Creating a New Route (Full Walkthrough)
- How Data Flows Through the App
- Built-in Features
- Testing & Benchmarking
- Environment Variables
Quick Start
Global Command (Recommended)
# 1. Install globally
npm i -g backend-express-generator
# 2. Scaffold the project using the small command
express-gen my-apiOr using npx
# 1. Scaffold the project without installing
npx backend-express-generator my-apiNext Steps
# 2. Move into it
cd my-api
# 3. Install dependencies
npm install
# 4. Start the dev server
npm run devYou should see:
🚀 Server running in development mode on http://localhost:5000
📖 API v1 available at http://localhost:5000/api/v1
❤️ Health check: http://localhost:5000/api/v1/healthVisit http://localhost:5000/api/v1/health — if it returns { "success": true } you are good to go.
Folder Structure — What Goes Where?
Think of it like a restaurant kitchen:
src/
├── config/ 🔧 Settings (DB connection, env vars, logger)
├── models/ 🗃️ Database table shapes (Mongoose schemas)
├── validations/ 🛡️ "Is the data correct?" checks (Zod schemas)
├── middlewares/ 🚦 Code that runs BEFORE your handler (auth, validation, rate-limit)
├── routes/ 🗺️ URL → handler mapping (what URL calls what function)
├── controllers/ 📬 Receives the HTTP request, sends the HTTP response
├── services/ 🧠 The actual business logic (talks to the DB)
└── utils/ 🔨 Reusable helpers (ApiError, ApiResponse, catchAsync)The Golden Rule: A controller should never touch the database directly. It calls a service. A service does the database work. This keeps things easy to test and maintain.
Request → Route → (Middleware) → Controller → Service → Database
↓
Response ←────────────────────────────────── Controller ←─Step 1 — Setting Up MongoDB
Option A — MongoDB Atlas (Cloud, Recommended for Beginners)
- Go to cloud.mongodb.com and create a free account.
- Click "Create a cluster" → choose the free M0 tier.
- In Database Access, create a user with a username and password.
- In Network Access, click "Allow Access from Anywhere" (
0.0.0.0/0) for development. - Click "Connect" → "Connect your application" → copy the connection string.
It looks like:
mongodb+srv://youruser:[email protected]/my-api?retryWrites=true&w=majority- Paste it into your
.envfile:
MONGO_URI=mongodb+srv://youruser:[email protected]/my-api?retryWrites=true&w=majorityOption B — Local MongoDB
Install MongoDB Community Server, then set:
MONGO_URI=mongodb://localhost:27017/my-apiOption C — Docker (One-liner)
docker run -d -p 27017:27017 --name mongo mongo:7MONGO_URI=mongodb://localhost:27017/my-apiHow does the app connect?
src/config/db.jscallsmongoose.connect(env.MONGO_URI)automatically when the server starts. If the connection fails, the app exits immediately so you never run with a broken database.
Step 2 — Creating a New Route (Full Walkthrough)
Let's say you want to build a Products API with GET /api/v1/products and POST /api/v1/products.
The 5-File Pattern
Every new resource follows this pattern. You always create these 5 files:
src/models/product.model.js ← shape of the data in MongoDB
src/validations/product.validation.js← rules for incoming request data
src/services/product.service.js ← database logic
src/controllers/product.controller.js← HTTP in, HTTP out
src/routes/v1/product.route.js ← URL definitionsThen you register the route in src/routes/v1/index.js.
Real Example: Products API
File 1 — src/models/product.model.js
This is the shape of your data in MongoDB. Think of it as defining a "table" with columns and rules.
import mongoose from 'mongoose';
const productSchema = new mongoose.Schema(
{
name: {
type: String,
required: [true, 'Product name is required'], // error message if missing
trim: true,
},
price: {
type: Number,
required: [true, 'Price is required'],
min: [0, 'Price cannot be negative'],
},
stock: {
type: Number,
default: 0,
},
isActive: {
type: Boolean,
default: true,
},
},
{ timestamps: true } // adds createdAt + updatedAt automatically
);
export const Product = mongoose.model('Product', productSchema);File 2 — src/validations/product.validation.js
This runs before your controller. It checks if the request body has the correct data. If not, it sends back a clear error — no bad data ever reaches your database.
import { z } from 'zod';
// Rules for POST /products (creating a product)
export const createProductSchema = z.object({
name: z.string({ required_error: 'Name is required' }).min(2).trim(),
price: z.number({ required_error: 'Price is required' }).min(0),
stock: z.number().min(0).optional().default(0),
});
// Rules for PATCH /products/:id (updating a product — all fields optional)
export const updateProductSchema = z.object({
name: z.string().min(2).trim().optional(),
price: z.number().min(0).optional(),
stock: z.number().min(0).optional(),
});Why Zod? It gives you crystal-clear error messages like
"price: Expected number, received string"instead of a cryptic crash.
File 3 — src/services/product.service.js
The service is the brain. It contains all database queries. Controllers call services — they don't touch the DB directly.
import { Product } from '../models/product.model.js';
import { ApiError } from '../utils/ApiError.js';
// Get all active products
export const getAllProducts = async () => {
return Product.find({ isActive: true }).lean();
};
// Create a new product
export const createProduct = async (data) => {
return Product.create(data);
};
// Get one product by ID — throws 404 if not found
export const getProductById = async (id) => {
const product = await Product.findById(id).lean();
if (!product) throw new ApiError(404, 'Product not found');
return product;
};Why a separate service file? Imagine your API grows and you need to send an email every time a product is created. You add that logic in the service — not the controller. The controller doesn't care about email, it just calls
createProduct()and returns the result.
File 4 — src/controllers/product.controller.js
The controller is a thin layer. It only does two things:
- Reads data from the request (
req.body,req.params). - Sends back a response (
res.json()).
All the real work is delegated to the service.
import * as ProductService from '../services/product.service.js';
import { ApiResponse } from '../utils/ApiResponse.js';
import { catchAsync } from '../utils/catchAsync.js';
// GET /api/v1/products
export const getAllProducts = catchAsync(async (req, res) => {
const products = await ProductService.getAllProducts();
res.status(200).json(new ApiResponse(200, products, 'Products fetched'));
});
// POST /api/v1/products
export const createProduct = catchAsync(async (req, res) => {
const product = await ProductService.createProduct(req.body);
res.status(201).json(new ApiResponse(201, product, 'Product created'));
});
// GET /api/v1/products/:id
export const getProductById = catchAsync(async (req, res) => {
const product = await ProductService.getProductById(req.params.id);
res.status(200).json(new ApiResponse(200, product, 'Product fetched'));
});What is
catchAsync? Without it you'd need to wrap every function intry { } catch(err) { next(err) }.catchAsyncdoes that for you automatically, keeping controllers clean.
File 5 — src/routes/v1/product.route.js
This file maps URLs to controller functions. It also chains the validation middleware so bad data is rejected before the controller is even called.
import { Router } from 'express';
import * as productController from '../../controllers/product.controller.js';
import { validate } from '../../middlewares/validate.middleware.js';
import { createProductSchema, updateProductSchema } from '../../validations/product.validation.js';
const router = Router();
router
.route('/')
.get(productController.getAllProducts)
.post(validate(createProductSchema), productController.createProduct);
// ↑ validates req.body before createProduct runs
router
.route('/:id')
.get(productController.getProductById);
export default router;Final Step — Register the route in src/routes/v1/index.js
Open src/routes/v1/index.js and add two lines:
import { Router } from 'express';
import healthRouter from './health.route.js';
import userRouter from './user.route.js';
import productRouter from './product.route.js'; // ← ADD THIS
const router = Router();
router.use('/health', healthRouter);
router.use('/users', userRouter);
router.use('/products', productRouter); // ← AND THIS
export default router;That's it! Your new endpoints are live:
GET http://localhost:5000/api/v1/productsPOST http://localhost:5000/api/v1/productsGET http://localhost:5000/api/v1/products/:id
How Data Flows Through the App
Here is what happens when someone sends POST /api/v1/products with a body of { "name": "Laptop", "price": 999 }:
1. Request arrives at Express
↓
2. app.js runs Helmet (security headers)
↓
3. app.js runs CORS check (is this origin allowed?)
↓
4. app.js runs Rate Limiter (has this IP sent too many requests?)
↓
5. Route matched: POST /api/v1/products → product.route.js
↓
6. validate(createProductSchema) middleware runs:
✅ "name" is a string ✓
✅ "price" is a number ✓
→ req.body is now safe, typed data
↓
7. productController.createProduct() runs
↓
8. ProductService.createProduct(req.body) is called
↓
9. MongoDB saves the document
↓
10. Controller sends: 201 { success: true, data: { ... }, message: "Product created" }If step 6 fails (e.g., price is missing):
validate middleware → throws ApiError(422, "Validation failed", [...errors])
↓
errorHandler middleware catches it → sends clean JSON error responseThe controller is never reached — no bad data touches your database.
Built-in Features
| Feature | How It Works |
|---|---|
| Security | helmet() sets safe HTTP headers automatically |
| CORS | Only origins in ALLOWED_ORIGINS env var are allowed |
| Rate Limiting | apiLimiter: 100 req/15 min; authLimiter: 10 req/15 min for auth routes |
| HTTP Logging | morgan logs every request — routed through Winston so one output stream |
| App Logging | winston for all structured logs — colourised in dev, JSON in prod |
| Env Validation | App crashes immediately on startup if PORT or MONGO_URI are missing |
| Error Handling | All errors (Mongoose, Zod, custom) end up as consistent JSON |
| Soft Delete | Users are never actually deleted — isActive: false is set instead |
| Graceful Shutdown | Server drains connections on SIGTERM before exiting |
| Load Testing | autocannon script included — fire npm run benchmark to test performance |
🧪 Testing the Server
Once your server is running (npm run dev), you can test it manually or use the built-in benchmarking tools.
1. Manual Testing (cURL Examples)
Here are some examples of how to interact with the API using curl (you can also use Postman, Insomnia, or Thunder Client).
Health Check:
curl -X GET http://localhost:5000/api/v1/health
# Response: {"success":true,"message":"Server is healthy","data":{"status":"OK","timestamp":"..."}}Create a User (POST):
curl -X POST http://localhost:5000/api/v1/users \
-H "Content-Type: application/json" \
-d '{"name": "Alice", "email": "[email protected]", "password": "Password123"}'Get All Users (GET):
curl -X GET "http://localhost:5000/api/v1/users?page=1&limit=10"Validation Error Example (Missing Email):
curl -X POST http://localhost:5000/api/v1/users \
-H "Content-Type: application/json" \
-d '{"name": "Bob", "password": "Password123"}'
# Response: 422 Unprocessable Entity
# {"success":false,"message":"Validation failed","errors":[{"field":"email","message":"Email is required"}]}2. Morgan — HTTP Request Logs
Morgan is an HTTP request logger. Every time a request hits your server (like the curl commands above), Morgan automatically prints a one-liner to the console. You don't have to add any logging code to your routes.
It's already wired up in src/app.js and feeds into Winston, so all logs go to the same place.
What you see in development:
2026-04-09 09:00:01 [http]: GET /api/v1/health 200 3.412 ms - 89
2026-04-09 09:00:02 [http]: POST /api/v1/users 201 45.231 ms - 210
2026-04-09 09:00:03 [http]: GET /api/v1/users/abc123 404 2.100 ms - 67This tells you:
- Method —
GET,POST,PATCH, etc. - Route — which URL was hit
- Status code —
200(OK),201(Created),404(Not Found). - Response time — how long the server took to respond.
- Response size — how many bytes were sent back.
In production, Morgan switches to Apache combined format — a structured log line that includes the client IP, timestamp, and user agent. Morgan is zero-config — just start your server and it works.
3. Autocannon — Load Testing
Autocannon stress-tests your API by firing hundreds of concurrent requests at it, then tells you how well your server held up. Think of it as checking: "If 10 users all hit my API at the same time for 10 seconds, does it break?"
How to run a benchmark
# Terminal 1 — start your server
npm run dev
# Terminal 2 — run the load test
npm run benchmarkTarget a specific route:
# Benchmark the users endpoint
node scripts/benchmark.js /api/v1/usersReading the results
🔫 Autocannon Load Test
─────────────────────────────────────────
🎯 Target : http://localhost:5000/api/v1/health
⏱️ Duration : 10 seconds
👥 Connections : 10 concurrent
─────────────────────────────────────────
📊 Summary
─────────────────────────────────────────
Requests/sec : 4823 avg | 5210 max
Latency (ms) : 2.04 avg | 12 p99 | 45 max
Throughput : 612.30 KB/s avg
Total requests : 48230
Errors : 0
─────────────────────────────────────────
🎉 No errors! Your server handled the load cleanly.What each number means:
| Metric | What it means | Good range |
|---|---|---|
| Requests/sec | How many requests/sec your server handled | Higher = better |
| Latency avg | Average time to get a response | < 50ms for a simple API |
| Latency p99 | 99% of requests responded within this time | < 200ms |
| Errors | Any 5xx or connection failures | Should be 0 |
Tip: If you see errors or high latency, check your MongoDB queries — slow DB reads are the #1 culprit. Use indexes on fields you query often (like
Environment Variables
Copy .env.example to .env and fill in your values:
# Which mode to run in. Changes log format and error verbosity.
NODE_ENV=development
# The port your server listens on.
PORT=5000
# Your MongoDB connection string (see "Setting Up MongoDB" above).
MONGO_URI=mongodb://localhost:27017/my-api
# Comma-separated list of frontend origins that are allowed to call this API.
ALLOWED_ORIGINS=http://localhost:3000,http://localhost:5173Never commit your
.envfile. It is already in.gitignorefor you.
License
MIT
