Node, the difference between development and production
By Flavio Copes
What NODE_ENV does in Node.js, where to set it for production (systemd, pm2, Docker, hosting), how Express reacts to it, and the npm devDependencies gotcha.
You can run the same Node.js app with different settings in development and in production. The switch is the NODE_ENV environment variable, and the only value that matters is production.
NODE_ENV is a convention. Node itself does nothing with it. process.env.NODE_ENV is undefined until you, or your hosting platform, set it. Libraries read it and change their behavior when the value is production.
What changes in production
Express is the classic example. Its env setting defaults to process.env.NODE_ENV, or development when the variable is missing. In production Express turns on the view cache setting, so templates are compiled once instead of on every request, and error messages get shorter. The Express docs say that setting NODE_ENV=production alone can make an app about three times faster.
Pug, the template engine many Express apps use, compiles templates in debug mode unless Express runs in production.
Outside of servers, NODE_ENV is how the React package decides whether to load its development build, with all the warnings and checks, or the much smaller production build. Bundlers set the value for you: vite build and next build both run with NODE_ENV=production, and that’s how React ends up in the browser as its production build. Next.js only accepts development, production and test as values.
How to set NODE_ENV
For one run, prepend it to the command:
NODE_ENV=production node app.js
This works in bash, zsh and fish. To set it for the whole terminal session, use export:
export NODE_ENV=production
In fish the syntax is set -gx NODE_ENV production, and in PowerShell on Windows it’s $env:NODE_ENV = "production". I wrote more about this in how to set environment variables in bash and zsh.
If a package.json script needs to set it and your team is on mixed operating systems, the cross-env package normalizes the syntax:
"start": "cross-env NODE_ENV=production node app.js"
Don’t put export NODE_ENV=production in your shell config file. On your laptop it turns every local run into a production run, and you lose the errors you need while developing. On a server it does nothing useful, because a service started by systemd or pm2 does not read your .zshrc.
Where to set it on a server
Set the variable where the process starts.
With systemd, add it to the unit file:
[Service]
Environment=NODE_ENV=production
ExecStart=/usr/bin/node /var/www/app/app.js
With pm2, put it in the ecosystem file:
module.exports = {
apps: [
{
name: 'app',
script: 'app.js',
env_production: {
NODE_ENV: 'production'
}
}
]
}
and start the app with pm2 start ecosystem.config.js --env production.
In a Dockerfile:
ENV NODE_ENV=production
NODE_ENV is not a secret, so it can live in the unit file or in your platform’s settings. Real secrets, like API keys and database passwords, belong in the platform’s secret store or in a tool like 1Password Developer Environments.
Hosting platforms differ, so check the docs of the one you use. Railway and Heroku set NODE_ENV=production for you at runtime. Vercel and Netlify don’t set it at all. On Vercel the framework sets it (next build uses production), and Netlify’s build leaves it undefined. A fresh VPS never has it, so when you self-host you set it yourself with one of the methods above.
Check it in your app
Read the value from process.env and compare it with production:
const isProd = process.env.NODE_ENV === 'production'
Notice that everything else counts as development. That matches how libraries do it, and it means a missing variable falls back to the verbose behavior you want on your laptop.
Many apps put this in a small config.js module, so the rest of the code never touches process.env:
export const isProd = process.env.NODE_ENV === 'production'
export const isDev = !isProd
If your app reads other variables too, see how to read environment variables from Node.js. If you keep them in a .env file, Node loads it with node --env-file=.env app.js, and I cover the details in how to use .env files in Node.js.
Branching in Express
Express 4 removed the old app.configure() hooks, so in Express 4 and 5 you branch on the value yourself.
The typical case is the error handler. While developing you want the full stack trace in the response. In production you want a short message, because a stack trace leaks file paths and library internals to whoever triggered the error.
Here is a complete app with a route that throws on purpose:
import express from 'express'
const app = express()
const isProd = process.env.NODE_ENV === 'production'
app.get('/', (req, res) => {
res.send('ok')
})
app.get('/boom', () => {
throw new Error('database connection refused')
})
app.use((err, req, res, next) => {
console.error(err.message)
if (isProd) {
res.status(500).send('Something went wrong')
return
}
res.status(500).send(err.stack)
})
app.listen(3000)
Run it with node app.js and open http://localhost:3000/boom. The response is the stack trace:
Error: database connection refused
at file:///Users/flavio/app/app.js:11:9
at Layer.handleRequest (/Users/flavio/app/node_modules/router/lib/layer.js:152:17)
Stop it and run NODE_ENV=production node app.js. The same URL now returns Something went wrong, and the error still reaches your server logs through console.error(). This is Express 5 (5.2.1 at the time of this update), and the same code works on Express 4.
The same isProd flag works for other middleware, for example a request logger that prints every request locally and stays quiet in production. My free Express course goes through middleware and error handling step by step, and this post is also a lesson in the free Node.js course.
Should you use NODE_ENV for staging?
No. The Node.js docs are explicit: always run with NODE_ENV=production, and treat any other value on a deployed server as an antipattern.
A staging server with NODE_ENV=staging runs different code than production. Express stops caching views, React loads its development build, and your own isProd checks turn false. What you test on staging is not what you ship.
Keep NODE_ENV as the “optimized or not” switch. For anything that depends on which server you’re on, use a separate variable, for example APP_ENV=staging. Vercel does the same with VERCEL_ENV, and Netlify with CONTEXT.
Two gotchas
The first one is about installing. When NODE_ENV is production, npm install skips devDependencies, because npm’s omit option defaults to dev in that case. With prettier in devDependencies:
NODE_ENV=production npm install
node_modules/prettier is missing. Run it again with --include=dev and npm installs it.
This is exactly what happens when you add NODE_ENV=production to a Netlify or Vercel project’s environment settings and the build suddenly can’t find the TypeScript compiler or Tailwind. If the build needs dev dependencies, run npm install --include=dev, or set the variable only for the runtime. Railway handles this for you: its builder sets NODE_ENV=production for the app and NPM_CONFIG_PRODUCTION=false for the install.
The second gotcha is forgetting the variable on a live server. Nothing breaks, so nobody notices. Express recompiles every template on every request, libraries keep their debug checks on, and error pages show stack traces to visitors. If a Node app feels slow in production, check this first.
Want me to talk about your product? You can sponsor this site.
Related posts about node: