The Node.js Setup I Use on Every New Project (2026 Edition)
Stop wasting time configuring the same things every time. Here's my starter template.
The Problem
Every new project, I'd spend 30-60 minutes on the same setup:
- ESLint config (and arguing with Prettier about it)
- Environment variables handling
- Error logging
- Graceful shutdown
- Health check endpoint
- Docker/PM2/systemd config
After doing this 20+ times, I finally created a template. Here it is.
Project Structure
my-project/
βββ src/
β βββ index.js # Entry point
β βββ config.js # Env vars + defaults
β βββ middleware/
β β βββ errorHandler.js
β β βββ notFound.js
β βββ routes/
β β βββ health.js
β βββ utils/
β βββ logger.js
βββ tests/
βββ .env.example
βββ .eslintrc.json
βββ .prettierrc
βββ .gitignore
βββ package.json
βββ server.js # For running directly
βββ guardian.sh # Auto-restart script
The Essentials
1. Config (src/config.js)
require('dotenv').config();
module.exports = {
port: parseInt(process.env.PORT) || 3000,
nodeEnv: process.env.NODE_ENV || 'development',
// Database (if needed)
db: {
url: process.env.DATABASE_URL || './data.db',
},
// External services
api: {
key: process.env.API_KEY,
baseUrl: process.env.API_BASE_URL,
},
// Feature flags
features: {
enableCache: process.env.ENABLE_CACHE === 'true',
enableRateLimit: process.env.ENABLE_RATE_LIMIT !== 'false',
},
};
Why: Centralized config means one place to check when debugging. No more hunting through files for where PORT is defined.
2. Logger (src/utils/logger.js)
const fs = require('fs');
const path = require('path');
const LOG_DIR = path.join(__dirname, '../../logs');
if (!fs.existsSync(LOG_DIR)) fs.mkdirSync(LOG_DIR, { recursive: true });
const today = new Date().toISOString().split('T')[0];
const logFile = fs.createWriteStream(path.join(LOG_DIR, `${today}.log`), { flags: 'a' });
const levels = { DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3 };
const minLevel = levels[process.env.LOG_LEVEL] || levels.INFO;
function log(level, ...args) {
if (levels[level] < minLevel) return;
const timestamp = new Date().toISOString();
const prefix = `[${timestamp}] [${level}]`;
const message = args.map(a =>
typeof a === 'object' ? JSON.stringify(a) : a
).join(' ');
const line = `${prefix} ${message}\n`;
// Console (colorized for dev)
if (process.env.NODE_ENV !== 'production') {
const colors = { DEBUG: '\x1b[36m', INFO: '\x1b[32m', WARN: '\x1b[33m', ERROR: '\x1b[31m' };
console.log(`${colors[level] || ''}${line}\x1b[0m`);
}
// File (always)
logFile.write(line);
}
module.exports = {
debug: (...a) => log('DEBUG', ...a),
info: (...a) => log('INFO', ...a),
warn: (...a) => log('WARN', ...a),
error: (...a) => log('ERROR', ...a),
};
Why: Console.log is fine for tutorials. Real apps need timestamps, log levels, file output, and production vs dev behavior.
3. Error Handler (src/middleware/errorHandler.js)
const logger = require('../utils/logger');
module.exports = (err, req, res, next) => {
logger.error({
message: err.message,
stack: err.stack,
path: req.path,
method: req.method,
body: req.body,
query: req.query,
ip: req.ip,
headers: req.headers,
});
// Don't leak stack traces in production
const response = {
error: process.env.NODE_ENV === 'production'
? 'Internal server error'
: err.message,
};
if (process.env.NODE_ENV !== 'production') {
response.stack = err.stack;
}
const status = err.status || err.statusCode || 500;
res.status(status).json(response);
};
Why: Consistent error responses make debugging easier and prevent info leaks in production.
4. Health Check (src/routes/health.js)
const router = require('express').Router();
router.get('/', (req, res) => {
const uptime = process.uptime();
const memory = process.memoryUsage();
res.json({
status: 'ok',
uptime: {
seconds: Math.floor(uptime),
human: formatUptime(uptime),
},
memory: {
rss: `${Math.round(memory.rss / 1024 / 1024)}MB`,
heapUsed: `${Math.round(memory.heapUsed / 1024 / 1024)}MB`,
heapTotal: `${Math.round(memory.heapTotal / 1024 / 1024)}MB`,
external: `${Math.round(memory.external / 1024 / 1024)}MB`,
},
process: {
pid: process.pid,
node: process.version,
platform: process.platform,
},
timestamp: new Date().toISOString(),
});
});
function formatUptime(seconds) {
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = Math.floor(seconds % 60);
return `${h}h ${m}m ${s}s`;
}
module.exports = router;
Why: Every monitoring tool needs an endpoint to ping. This one gives useful debug info too.
5. Graceful Shutdown (src/index.js)
const express = require('express');
const config = require('./config');
const logger = require('./utils/logger');
const healthRouter = require('./routes/health');
const errorHandler = require('./middleware/errorHandler');
const app = express();
// Middleware
app.use(express.json());
app.use('/health', healthRouter);
// Your routes here
// app.use('/api/your-route', yourRouter);
// Error handler (must be last)
app.use(errorHandler);
const server = app.listen(config.port, () => {
logger.info(`Server started on port ${config.port}`);
});
// Graceful shutdown
function shutdown(signal) {
logger.info(`${signal} received, shutting down gracefully...`);
server.close(() => {
logger.info('HTTP server closed');
process.exit(0);
});
// Force exit after 10 seconds
setTimeout(() => {
logger.error('Forced shutdown after 10s timeout');
process.exit(1);
}, 10000);
}
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));
// Handle uncaught exceptions
process.on('uncaughtException', (err) => {
logger.error('UNCAUGHT EXCEPTION:', err);
shutdown('uncaughtException');
});
process.on('unhandledRejection', (reason, promise) => {
logger.error('UNHANDLED REJECTION:', reason);
});
Why: Without graceful shutdown, active connections get dropped. With it, connections finish, cleanup runs, logs flush.
6. Guardian Script (guardian.sh)
#!/bin/bash
PROJECT_DIR="$(cd "$(dirname "$0")" && pwd)"
PORT="${1:-3000}"
# Check if process is running on expected port
if ! ss -tlnp | grep -q ":${PORT}"; then
echo "$(date): Process not found on port ${PORT}, starting..."
cd "$PROJECT_DIR"
export PATH="/root/.nvm/current/bin:$PATH"
# Kill any zombie processes
pkill -f "node.*server.js" 2>/dev/null || true
sleep 1
# Start fresh
nohup node server.js >> ./logs/server.log 2>&1 &
echo "$(date): Started with PID $!"
else
echo "$(date): Process running on port ${PORT}"
fi
Why: Crashes happen. This brings your app back within 5 minutes without any human intervention.
7. Package.json Scripts
{
"scripts": {
"start": "node server.js",
"dev": "node --watch server.js",
"test": "node --test",
"lint": "eslint src/",
"format": "prettier --write 'src/**/*.js'"
},
"dependencies": {
"express": "^4.21.0",
"dotenv": "^16.4.0"
},
"devDependencies": {
"eslint": "^9.0.0",
"prettier": "^3.3.0"
}
}
Why: Consistent scripts mean you never forget how to run your own project.
What's NOT Included (And Why)
- Docker: Overkill for single-server deployments. Add it when you need multi-environment parity.
- TypeScript: Great for large teams. For solo projects, JS + JSDoc is faster.
- ORM: SQLite with raw SQL is fine up to ~100K rows. Add Prisma/Drizzle later.
- Testing framework: Node's built-in
node:testis sufficient for most projects. - CI/CD: GitHub Actions is free for public repos. Add it when you have a team.
The Full Template
All of this fits in under 200 lines of code (excluding comments). You can copy it, modify it, and have a production-ready project skeleton in minutes instead of hours.
The real value isn't the code β it's the decisions encoded in it:
- How to handle errors consistently
- How to log properly from day one
- How to survive crashes automatically
- How to monitor health without external tools
These are things you learn after production incidents. Save yourself the pain and start with them day one.
What's in YOUR project template? Anything I'm missing? Drop a comment β let's compare setups.
Follow @armorbreak for more practical Node.js guides.
Mid-Year Sale β Unlock Full Article
Base plan from just $4.99/mo or $49/yr
Sign in to read the full article and unlock all tutorials.
Sign In / Register β Start Free Trial7-day free trial Β· Cancel anytime Β· 30-day money-back
