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

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

If folks want it, I can zip this into a starter repo with OpenAPI, basic tests, and Docker. Leave a comment below!