Express.js is a minimal and flexible Node.js web application framework that provides a robust set of features for web and mobile applications. From startups to large-scale enterprises, it's a go-to framework for building APIs and web services. However, when it comes to deploying Express.js applications into production, developers often fall into a few traps—some minor, some catastrophic.
1. Not Setting NODE_ENV=production
What Happens
One of the most overlooked deployment best practices is setting the environment variable NODE_ENV
to "production"
. Express uses this variable to determine the environment your app is running in.
By default, if NODE_ENV
is not set, Express assumes a development environment. This means your app could:
- Log excessive information (which can expose sensitive data)
- Skip caching templates or other optimizations
- Not leverage compression or performance improvements
Why It Matters
Running in production mode enables several internal optimizations, such as caching view templates and reducing error message verbosity, which directly affect performance and security.
How to Fix It
In your deployment script or service configuration, always set:
NODE_ENV=production
For example, in a typical Linux environment:
NODE_ENV=production node app.js
In Docker, define it in your Dockerfile:
ENV NODE_ENV=production
Or if you're using PM2:
pm2 start app.js --env production
Pro Tip
You can verify it in your code:
console.log('Environment:', process.env.NODE_ENV);
2. Forgetting to Secure HTTP Headers
What Happens
Out of the box, Express doesn't secure your app against common web vulnerabilities like XSS, content sniffing, clickjacking, etc. That’s because security is a developer's responsibility, and Express stays minimal by default.
Why It Matters
Without proper headers:
- Browsers may allow unsafe rendering behavior
- Attackers can more easily exploit known web vulnerabilities
- Your server might reveal information like
X-Powered-By: Express
, giving away your tech stack
How to Fix It
Use the helmet
middleware. It’s a collection of security headers you can plug in with a single line of code:
npm install helmet
const helmet = require('helmet');
app.use(helmet());
This helps set important headers like:
Content-Security-Policy
X-Content-Type-Options
Strict-Transport-Security
X-Frame-Options
Pro Tip
Turn off Express' X-Powered-By
header to hide your server type:
app.disable('x-powered-by');
3. Not Using a Process Manager
What Happens
If you start your Express app with:
node app.js
...then your app will crash the moment:
- It runs out of memory
- An unhandled exception occurs
- Your server reboots
Why It Matters
In production, you must ensure your app restarts automatically on crash, supports zero-downtime deployments, and can scale across CPU cores.
How to Fix It
Use a process manager like PM2, which:
- Restarts your app on crash
- Manages logs
- Supports cluster mode (multi-core)
- Allows graceful reloads
Install PM2:
npm install -g pm2
Start your app:
pm2 start app.js --name my-express-app
Save your process list:
pm2 save
Startup script on system reboot:
pm2 startup
Pro Tip
Enable cluster mode to use multiple cores:
pm2 start app.js -i max
4. Serving Static Files Inefficiently
What Happens
If you're serving static assets like images, stylesheets, or frontend JavaScript directly through Express with:
app.use(express.static('public'));
…it might work for small apps, but it’s not optimized for production. Express isn’t a full-fledged static asset server like Nginx or a CDN.
Why It Matters
Static files can:
- Consume server memory and bandwidth
- Slow down your backend responsiveness
- Prevent HTTP caching and compression
How to Fix It
Use Nginx or a CDN (Cloudflare, AWS CloudFront, etc.)
Let a reverse proxy like Nginx handle your static files:
location /static/ {
root /var/www/your-app;
expires 30d;
add_header Cache-Control "public";
}
Or set strong cache headers in Express:
app.use(express.static('public', {
maxAge: '30d',
etag: false
}));
Also, compress files using compression
middleware:
npm install compression
const compression = require('compression');
app.use(compression());
Pro Tip
Always move large files (videos, high-res images) to a cloud storage (S3, GCP Buckets) or CDN.
5. Ignoring Proper Logging and Monitoring
What Happens
Developers often deploy Express apps with console.log()
as their main logging tool. While it’s fine during development, it’s insufficient for production.
You’ll face issues like:
- No error tracking
- No performance insights
- No audit trail of issues leading to crashes
Why It Matters
Without monitoring, you can’t diagnose issues, detect anomalies, or even know when your app goes down.
How to Fix It
Use a logging library:
Example with Winston:
npm install winston
const winston = require('winston');
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [new winston.transports.Console()]
});
app.use((req, res, next) => {
logger.info(`${req.method} ${req.url}`);
next();
});
Integrate Monitoring Tools
For example, with Sentry:
npm install @sentry/node
const Sentry = require("@sentry/node");
Sentry.init({ dsn: "YOUR_SENTRY_DSN" });
app.use(Sentry.Handlers.requestHandler());
Pro Tip
Don’t forget health checks:
app.get('/health', (req, res) => res.status(200).send('OK'));
Use tools like UptimeRobot or BetterUptime to monitor them.
6. Hardcoding Secrets and Config in Codebase
What Happens
One of the most dangerous mistakes is hardcoding sensitive values like:
const dbPassword = "mySuperSecretPassword123";
const jwtSecret = "thisIsTopSecret";
This becomes especially risky when code is pushed to GitHub or shared across environments.
Why It Matters
Leaked credentials can:
- Give attackers access to your database
- Expose your API keys or tokens
- Lead to financial and reputational loss
How to Fix It
Use Environment Variables
Use a .env
file in development (never in production):
DB_PASSWORD=mySuperSecretPassword123
JWT_SECRET=thisIsTopSecret
Read them in your code:
npm install dotenv
require('dotenv').config();
const dbPassword = process.env.DB_PASSWORD;
const jwtSecret = process.env.JWT_SECRET;
On production servers (or in Docker, CI/CD), set environment variables outside the code.
Use Secret Managers in Production
- AWS Secrets Manager
- HashiCorp Vault
- Google Secret Manager
- Docker Secrets
Example in Docker:
ENV JWT_SECRET=topsecret
Pro Tip
Always add .env
to .gitignore
:
.env
Final Thoughts
Deploying an Express.js application isn't just about running node app.js
on a server. To truly go production-grade, you need to think like a DevOps engineer, security specialist, and performance optimizer—all at once.
Recap of What You Should Do:
Mistake | Solution |
---|---|
Not setting NODE_ENV |
Use NODE_ENV=production |
No security headers | Use helmet , disable x-powered-by |
No process manager | Use PM2 or similar |
Inefficient static file handling | Offload to Nginx/CDN, use compression |
Poor logging | Use winston , sentry , and monitoring tools |
Hardcoded secrets | Use .env files and secret managers |
You may also like:
Read more blogs from Here
Share your experiences in the comments, and let's discuss how to tackle them!
Follow me on LinkedIn