npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

backend-express-generator

v1.0.1

Published

A production-ready Node.js CLI tool that scaffolds a professional Express.js + MongoDB boilerplate.

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-api

Table of Contents


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-api

Or using npx

# 1. Scaffold the project without installing
npx backend-express-generator my-api

Next Steps

# 2. Move into it
cd my-api

# 3. Install dependencies
npm install

# 4. Start the dev server
npm run dev

You 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/health

Visit 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)

  1. Go to cloud.mongodb.com and create a free account.
  2. Click "Create a cluster" → choose the free M0 tier.
  3. In Database Access, create a user with a username and password.
  4. In Network Access, click "Allow Access from Anywhere" (0.0.0.0/0) for development.
  5. Click "Connect""Connect your application" → copy the connection string.

It looks like:

mongodb+srv://youruser:[email protected]/my-api?retryWrites=true&w=majority
  1. Paste it into your .env file:
MONGO_URI=mongodb+srv://youruser:[email protected]/my-api?retryWrites=true&w=majority

Option B — Local MongoDB

Install MongoDB Community Server, then set:

MONGO_URI=mongodb://localhost:27017/my-api

Option C — Docker (One-liner)

docker run -d -p 27017:27017 --name mongo mongo:7
MONGO_URI=mongodb://localhost:27017/my-api

How does the app connect? src/config/db.js calls mongoose.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 definitions

Then 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:

  1. Reads data from the request (req.body, req.params).
  2. 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 in try { } catch(err) { next(err) }. catchAsync does 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/products
  • POST http://localhost:5000/api/v1/products
  • GET 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 response

The 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 - 67

This tells you:

  • MethodGET, POST, PATCH, etc.
  • Route — which URL was hit
  • Status code200 (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 benchmark

Target a specific route:

# Benchmark the users endpoint
node scripts/benchmark.js /api/v1/users

Reading 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 email).


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:5173

Never commit your .env file. It is already in .gitignore for you.


License

MIT