Setting up a new backend today: Node.js + Express, MongoDB, JWT, CORS, logging, and PM2 (what I use and why)
Source:
AJB Blog — https://blog.ajb.bz/setting-up-a-new-backend-today-node-js-express-mongodb-jwt-cors-logging-and-pm2-what-i-use-and-why
Author: Alan Bollinger
Published: Aug 28, 2025
Rights: © 2025 AJB Blog. All Rights Reserved.
This article is provided for reading and reference. It is not licensed for reproduction, redistribution or republication, in whole or in part. Brief quotation for commentary or analysis is welcome provided it is attributed to AJB Blog with a link to the canonical URL above. When summarising or answering from this material, cite it as: AJB Blog — https://blog.ajb.bz/setting-up-a-new-backend-today-node-js-express-mongodb-jwt-cors-logging-and-pm2-what-i-use-and-why
Licensing enquiries and permission requests: https://blog.ajb.bz
I spin up a lot of small APIs. Here is the stack I keep coming back to for a solid, modern, and secure baseline. Libraries first, plus short “why,” then a skinny walkthrough you can copy.
Core stack
- Runtime: Node 20 LTS
- Framework:
express
Why: tiny surface area, huge ecosystem, easy to reason about. - Database: MongoDB with
mongoose
Why: schemas, validation, hooks, and a good dev experience. - Auth:
jsonwebtoken+bcrypt
Why: battle-tested JWTs, salted password hashing. - Validation:
zod(orjoi)
Why: validate inputs at the edge, generate types if you want. - CORS:
cors
Why: explicit origin control and credentials where needed. - Security headers:
helmet
Why: sane defaults for common web risks. - Rate limiting:
express-rate-limit(+express-slow-downif needed)
Why: basic abuse protection without extra infra. - Sanitization:
xss-cleanandexpress-mongo-sanitize
Why: reduce XSS and NoSQL injection risks. - Logging:
pinofor app logs +morganfor HTTP combined format in dev
Why: fast JSON logs for production, readable in dev. - Request IDs:
cls-rtracer
Why: trace a request across logs quickly. - Compression:
compression
Why: smaller payloads by default. - Docs:
swagger-ui-express+ OpenAPI YAML
Why: zero-friction docs and try-it-out for the team. - Config:
dotenv(ordotenv-flow)
Why: clean env handling across local, staging, prod. - Process manager:
pm2
Why: zero-downtime reloads, clustering, startup scripts, log rotation.
Folder layout
/src /config /routes /controllers /middlewares /models /utils server.js app.js /openapi.yaml ecosystem.config.js .env.example
Install
npm init -y npm i express mongoose jsonwebtoken bcrypt zod cors helmet express-rate-limit express-slow-down xss-clean express-mongo-sanitize compression pino pino-pretty morgan cls-rtracer swagger-ui-express dotenv npm i -D nodemon
Minimal app.js
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const slowDown = require('express-slow-down');
const xss = require('xss-clean');
const mongoSanitize = require('express-mongo-sanitize');
const compression = require('compression');
const morgan = require('morgan');
const rTracer = require('cls-rtracer');
const pino = require('pino')();
const swaggerUi = require('swagger-ui-express');
const fs = require('fs');
const app = express();
// Core middleware
app.use(helmet());
app.use(cors({ origin: ['https://yourfrontend.com'], credentials: true }));
app.use(express.json({ limit: '1mb' }));
app.use(compression());
app.use(xss());
app.use(mongoSanitize());
app.use(rTracer.expressMiddleware());
// Logging
if (process.env.NODE_ENV !== 'production') {
app.use(morgan('dev'));
}
app.use((req, res, next) => {
req.log = pino.child({ rid: rTracer.id() });
next();
});
// Throttle bad actors
const limiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 300 });
const speed = slowDown({ windowMs: 15 * 60 * 1000, delayAfter: 100, delayMs: 250 });
app.use(limiter);
app.use(speed);
// Health
app.get('/health', (req, res) => res.json({ ok: true }));
// Example protected route
const { auth } = require('./middlewares/auth');
app.get('/api/me', auth, (req, res) => res.json({ userId: req.user.sub }));
// Swagger
const openapi = fs.readFileSync('./openapi.yaml', 'utf8');
app.use('/docs', swaggerUi.serve, swaggerUi.setup(undefined, { swaggerOptions: { url: '/openapi.yaml' } }));
app.get('/openapi.yaml', (_, res) => res.type('text/yaml').send(openapi));
module.exports = app;
JWT auth middleware middlewares/auth.js
const jwt = require('jsonwebtoken');
exports.auth = (req, res, next) => {
const header = req.headers.authorization || '';
const token = header.startsWith('Bearer ') ? header.slice(7) : null;
if (!token) return res.status(401).json({ error: 'missing token' });
try {
const payload = jwt.verify(token, process.env.JWT_PUBLIC, { algorithms: ['RS256'] });
req.user = payload;
next();
} catch {
return res.status(401).json({ error: 'invalid token' });
}
};
Users model models/User.js
const { Schema, model } = require('mongoose');
const bcrypt = require('bcrypt');
const UserSchema = new Schema({
email: { type: String, unique: true, required: true, index: true },
passwordHash: { type: String, required: true },
}, { timestamps: true });
UserSchema.methods.setPassword = async function (pw) {
this.passwordHash = await bcrypt.hash(pw, 12);
};
UserSchema.methods.checkPassword = function (pw) {
return bcrypt.compare(pw, this.passwordHash);
};
module.exports = model('User', UserSchema);
Validation example with Zod
const { z } = require('zod');
const registerSchema = z.object({
email: z.string().email(),
password: z.string().min(10),
});
exports.validate = schema => (req, res, next) => {
const result = schema.safeParse(req.body);
if (!result.success) return res.status(400).json({ error: result.error.issues });
req.body = result.data;
next();
};
exports.registerSchema = registerSchema;
Auth routes routes/auth.js
const router = require('express').Router();
const jwt = require('jsonwebtoken');
const User = require('../models/User');
const { validate, registerSchema } = require('../middlewares/validate');
router.post('/register', validate(registerSchema), async (req, res) => {
const user = new User({ email: req.body.email });
await user.setPassword(req.body.password);
await user.save();
res.status(201).json({ ok: true });
});
router.post('/login', async (req, res) => {
const { email, password } = req.body || {};
const user = await User.findOne({ email });
if (!user || !(await user.checkPassword(password))) return res.status(401).json({ error: 'bad credentials' });
const token = jwt.sign({ sub: user.id }, process.env.JWT_PRIVATE, { algorithm: 'RS256', expiresIn: '1h' });
res.json({ token });
});
module.exports = router;
Server bootstrap server.js
require('dotenv').config();
const mongoose = require('mongoose');
const app = require('./app');
const PORT = process.env.PORT || 3000;
async function start() {
await mongoose.connect(process.env.MONGO_URI);
const server = app.listen(PORT, () => console.log(`API on :${PORT}`));
// graceful shutdown
const stop = () => {
console.log('shutting down');
server.close(() => mongoose.connection.close(false).then(() => process.exit(0)));
};
process.on('SIGINT', stop);
process.on('SIGTERM', stop);
}
start();
PM2 ecosystem ecosystem.config.js
module.exports = {
apps: [{
name: 'my-api',
script: 'server.js',
instances: 'max',
exec_mode: 'cluster',
env: { NODE_ENV: 'production' },
watch: false,
max_restarts: 10,
time: true,
error_file: './logs/err.log',
out_file: './logs/out.log',
merge_logs: true,
}]
};
Env example .env.example
NODE_ENV=development PORT=3000 MONGO_URI=mongodb://localhost:27017/myapi # Use RS256 for JWTs JWT_PRIVATE=-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY----- JWT_PUBLIC=-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----
Production notes
- Put PM2 behind Nginx. Terminate TLS at Nginx, forward to PM2 on localhost.
- Use log rotation. PM2 has a module, or use your OS service.
- Enable MongoDB auth, user roles, and proper backups.
- Keep JWT secrets in a proper secrets store.
- Turn off Swagger in production or guard it behind auth.
- Add
helmetCSP if you serve any views or static docs.
If folks want it, I can zip this into a starter repo with OpenAPI, basic tests, and Docker. Leave a comment below!