DEV Community

Cover image for Backend Security for Express.js (With Nginx + VPS)
Khaled Md Saifullah
Khaled Md Saifullah

Posted on

Backend Security for Express.js (With Nginx + VPS)

Securing an Express.js backend is not just about adding a few middlewares. Real-world security is a layered system: application controls, reverse proxy protection and server level hardening.

This guide is a practical security blueprint for deploying Express.js APIs on a VPS with Nginx in front covering beginner to production grade controls.

Table of Contents

Threat Model

A typical public backend is targeted by:

  • Brute force login attempts
  • API abuse / scraping
  • Credential stuffing
  • Injection attacks
  • Misconfigured CORS
  • Token theft
  • Vulnerability scanners
  • Reverse-proxy bypass (direct port access)
  • Server compromise via weak SSH

The goal is to reduce risk using multiple independent layers.

Why Backend Security is Critical

Unlike static websites any applications expose APIs that interact directly with databases and user data. These APIs are a prime target for the attackers.

Threat Impact
NoSQL Injection Database compromise
Credential Stuffing Account takeover
JWT Theft Full user impersonation
CSRF Unauthorized actions
XSS Token and session leakage
API Abuse Server and payment exploitation

Security Baseline

Minimum production requirements

  • HTTPS enforced
  • Strong authentication strategy (JWT or sessions)
  • Strict CORS
  • Server-side validation
  • Rate limiting
  • Centralized logging
  • Nginx reverse-proxy protection
  • Firewall + SSH hardening
  • Fail2ban + automatic security patches

Project Setup

Install required dependencies

npm i express helmet cors express-rate-limit cookie-parser compression
npm i zod pino pino-http
npm i dotenv
Enter fullscreen mode Exit fullscreen mode

Application Layer Security

1) Security Headers

Use helmet to apply safe defaults.

import helmet from "helmet";

app.use(helmet());
Enter fullscreen mode Exit fullscreen mode

Note: For APIs, avoid over configuring CSP unless you serve web pages too.

2) CORS Strategy

Never use * origins in production, if you rely on cookies or credentials.

import cors from "cors";

const allowedOrigins = [
  "https://your-frontend.com",
  "https://www.your-frontend.com",
];

app.use(cors({
  origin: function (origin, cb) {
    if (!origin) return cb(null, true);
    if (allowedOrigins.includes(origin)) return cb(null, true);
    return cb(new Error("CORS blocked"), false);
  },
  credentials: true,
  methods: ["GET", "POST", "PUT", "PATCH", "DELETE"],
  allowedHeaders: ["Content-Type", "Authorization"]
}));
Enter fullscreen mode Exit fullscreen mode

3) Authentication & Authorization

  • Authentication proves identity
  • Authorization ensures access control

Best practice

  • short lived access tokens
  • rotate refresh tokens
  • role-based checks for admin routes

Example authorization guard:

export function requireRole(...roles) {
  return (req, res, next) => {
    if (!req.user || !roles.includes(req.user.role)) {
      return res.status(403).json({ status: false, message: "Forbidden" });
    }
    next();
  };
}
Enter fullscreen mode Exit fullscreen mode

4) Input Validation & Sanitization

Validate every request. Prefer schema validation (Zod).

import { z } from "zod";

const createUserSchema = z.object({
  name: z.string().min(2).max(80),
  email: z.string().email(),
  password: z.string().min(8).max(72),
});

export function validate(schema) {
  return (req, res, next) => {
    const result = schema.safeParse(req.body);
    if (!result.success) {
      return res.status(400).json({
        status: false,
        message: "Validation failed",
        errors: result.error.issues,
      });
    }
    req.body = result.data;
    next();
  };
}
Enter fullscreen mode Exit fullscreen mode

5) Rate Limiting

Use different limits per endpoint category:

  • global protection (general)
  • strict for auth endpoints
import rateLimit from "express-rate-limit";

export const globalLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 600,
  standardHeaders: true,
  legacyHeaders: false,
});

export const authLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 20,
  message: { message: "Too many login attempts. Try again later." }
});

app.use(globalLimiter);
app.use("/api/auth", authLimiter);
Enter fullscreen mode Exit fullscreen mode

6) Request Size Limits

Prevent payload abuse.

app.use(express.json({ limit: "200kb" }));
app.use(express.urlencoded({ extended: true, limit: "200kb" }));
Enter fullscreen mode Exit fullscreen mode

7) Logging & Audit

Use structured logging:

import pino from "pino";
import pinoHttp from "pino-http";

const logger = pino({ level: process.env.LOG_LEVEL || "info" });
app.use(pinoHttp({ logger }));
Enter fullscreen mode Exit fullscreen mode

Log:

  • auth failures
  • suspicious traffic spikes
  • rate limit blocks
  • Never log passwords, tokens or raw secrets

8) Secrets & Environment Security

  • store secrets in .env (devlopment) and in server environment (prod)
  • never commit .env
  • rotate compromised keys immediately

Example

NODE_ENV=production
PORT=3000
JWT_SECRET=YOUR-JWT-SECRET-KEY
Enter fullscreen mode Exit fullscreen mode

Reverse Proxy Layer (Nginx)

Your API should not be exposed directly to the internet. Production setup should be:

Internet → Nginx (443) → Express (127.0.0.1:8000)

1) TLS (HTTPS)

Install Certbot

sudo apt update
sudo apt install nginx certbot python3-certbot-nginx -y
Enter fullscreen mode Exit fullscreen mode

Issue certificate

sudo certbot --nginx -d api.yourdomain.com
Enter fullscreen mode Exit fullscreen mode

Auto renew

sudo systemctl status certbot.timer
Enter fullscreen mode Exit fullscreen mode

2) Nginx Rate Limiting

Add these in /etc/nginx/nginx.conf (inside http { })

limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
limit_conn_zone $binary_remote_addr zone=conn_limit:10m;
Enter fullscreen mode Exit fullscreen mode

Example server block

server {
  listen 443 ssl http2;
  server_name api.yourdomain.com;

  # Rate limiting
  limit_req zone=api_limit burst=20 nodelay;
  limit_conn conn_limit 20;

  location / {
    proxy_pass http://localhost:3000;
    proxy_http_version 1.1;

    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;

    # Timeout
    proxy_connect_timeout 10s;
    proxy_send_timeout 30s;
    proxy_read_timeout 30s;
  }
}
Enter fullscreen mode Exit fullscreen mode

3) Basic Bot/Scanner Reduction

Block common junk requests

location = /xmlrpc.php { return 444; }
location = /wp-login.php { return 444; }
location = /.env { return 444; }
Enter fullscreen mode Exit fullscreen mode

4) Recommended Security Headers at Nginx

add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
Enter fullscreen mode Exit fullscreen mode

VPS Hardening (Server Level)

SSH Hardening

  1. Create a non root user and give sudo permission
adduser deploy
usermod -aG sudo deploy
Enter fullscreen mode Exit fullscreen mode
  1. Disable root login & password auth
# edit on this file
sudo nano /etc/ssh/sshd_config

# set the values below in the config file 
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
Enter fullscreen mode Exit fullscreen mode

Restart SSH

sudo systemctl restart ssh
Enter fullscreen mode Exit fullscreen mode

Firewall (UFW)

Allow only what you need

sudo ufw allow OpenSSH
sudo ufw allow 80
sudo ufw allow 443
sudo ufw enable
sudo ufw status
Enter fullscreen mode Exit fullscreen mode

Important

  • Do not expose 8000 publicly
  • Ensure Express binds to 127.0.0.1

Fail2ban

  1. Install fail2ban
sudo apt install fail2ban -y
Enter fullscreen mode Exit fullscreen mode
  1. Enable basic protection for SSH
sudo systemctl enable --now fail2ban
sudo fail2ban-client status
Enter fullscreen mode Exit fullscreen mode

Automatic Security Updates

sudo apt install unattended-upgrades -y
sudo dpkg-reconfigure unattended-upgrades
Enter fullscreen mode Exit fullscreen mode

Deployment checklist

  • HTTPS enabled and forced
  • Express listens only on 127.0.0.1
  • Nginx proxies traffic to Express
  • CORS restricted to trusted origins
  • Input validation for all routes
  • Rate limiting enabled (app + nginx)
  • Firewall blocks unused ports
  • SSH hardened + key-based auth
  • Fail2ban active
  • Logs monitored and rotated
  • Secrets not committed

Final Security Rule

  • Never trust the frontend
  • Trust only the backend
  • Verify everything

Conclusion

Express.js backend security is a discipline rather than a particular configuration or module. Strong application level restrictions, a hardened reverse proxy and a properly locked down server environment are the components of real-world security.

You can greatly limit the attack surface of your API and safeguard your users, data, and business logic from real-world threats by putting in place tiered defenses with Express, Nginx and VPS level security.

For long term production use, a secure backend is not only safer but also more dependable, scalable and trustworthy.

Top comments (0)