claude-mpm 4.17.0__py3-none-any.whl → 4.17.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of claude-mpm might be problematic. Click here for more details.
- claude_mpm/VERSION +1 -1
- claude_mpm/skills/bundled/api-documentation.md +393 -0
- claude_mpm/skills/bundled/async-testing.md +571 -0
- claude_mpm/skills/bundled/code-review.md +143 -0
- claude_mpm/skills/bundled/database-migration.md +199 -0
- claude_mpm/skills/bundled/docker-containerization.md +194 -0
- claude_mpm/skills/bundled/express-local-dev.md +1429 -0
- claude_mpm/skills/bundled/fastapi-local-dev.md +1199 -0
- claude_mpm/skills/bundled/git-workflow.md +414 -0
- claude_mpm/skills/bundled/imagemagick.md +204 -0
- claude_mpm/skills/bundled/json-data-handling.md +223 -0
- claude_mpm/skills/bundled/nextjs-local-dev.md +807 -0
- claude_mpm/skills/bundled/pdf.md +141 -0
- claude_mpm/skills/bundled/performance-profiling.md +567 -0
- claude_mpm/skills/bundled/refactoring-patterns.md +180 -0
- claude_mpm/skills/bundled/security-scanning.md +327 -0
- claude_mpm/skills/bundled/systematic-debugging.md +473 -0
- claude_mpm/skills/bundled/test-driven-development.md +378 -0
- claude_mpm/skills/bundled/vite-local-dev.md +1061 -0
- claude_mpm/skills/bundled/web-performance-optimization.md +2305 -0
- claude_mpm/skills/bundled/xlsx.md +157 -0
- {claude_mpm-4.17.0.dist-info → claude_mpm-4.17.1.dist-info}/METADATA +1 -1
- {claude_mpm-4.17.0.dist-info → claude_mpm-4.17.1.dist-info}/RECORD +27 -7
- {claude_mpm-4.17.0.dist-info → claude_mpm-4.17.1.dist-info}/WHEEL +0 -0
- {claude_mpm-4.17.0.dist-info → claude_mpm-4.17.1.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.17.0.dist-info → claude_mpm-4.17.1.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.17.0.dist-info → claude_mpm-4.17.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,1429 @@
|
|
|
1
|
+
---
|
|
2
|
+
skill_id: express-local-dev
|
|
3
|
+
skill_version: 0.1.0
|
|
4
|
+
description: Running Express development servers with auto-reload tools like Nodemon, managing production deployments with PM2 clustering, and implementing graceful shutdown patterns.
|
|
5
|
+
updated_at: 2025-10-30T17:00:00Z
|
|
6
|
+
tags: [express, nodejs, development, server, backend]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Express Local Development Server
|
|
10
|
+
|
|
11
|
+
## Overview
|
|
12
|
+
|
|
13
|
+
Express is a minimal and flexible Node.js web application framework providing a robust set of features for web and mobile applications. This skill covers running Express development servers with auto-reload tools like Nodemon, managing production deployments with PM2 clustering, and implementing graceful shutdown patterns.
|
|
14
|
+
|
|
15
|
+
## When to Use This Skill
|
|
16
|
+
|
|
17
|
+
- Setting up Express development environment with auto-reload
|
|
18
|
+
- Configuring Nodemon for optimal development workflow
|
|
19
|
+
- Troubleshooting file watching and reload issues
|
|
20
|
+
- Managing Express production deployment with PM2
|
|
21
|
+
- Implementing graceful shutdown handlers
|
|
22
|
+
- Configuring zero-downtime reloads
|
|
23
|
+
- Coordinating multiple Express instances
|
|
24
|
+
- Understanding Nodemon vs PM2 trade-offs
|
|
25
|
+
|
|
26
|
+
## Quick Start
|
|
27
|
+
|
|
28
|
+
### Development Server
|
|
29
|
+
|
|
30
|
+
**Basic Express Server:**
|
|
31
|
+
```javascript
|
|
32
|
+
// server.js
|
|
33
|
+
const express = require('express');
|
|
34
|
+
const app = express();
|
|
35
|
+
const PORT = process.env.PORT || 3000;
|
|
36
|
+
|
|
37
|
+
app.get('/', (req, res) => {
|
|
38
|
+
res.json({ message: 'Hello World' });
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const server = app.listen(PORT, () => {
|
|
42
|
+
console.log(`Server running on port ${PORT}`);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Graceful shutdown
|
|
46
|
+
process.on('SIGTERM', () => {
|
|
47
|
+
console.log('SIGTERM received, closing server...');
|
|
48
|
+
server.close(() => {
|
|
49
|
+
console.log('Server closed');
|
|
50
|
+
process.exit(0);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**With Nodemon:**
|
|
56
|
+
```bash
|
|
57
|
+
# Install nodemon
|
|
58
|
+
npm install -D nodemon
|
|
59
|
+
|
|
60
|
+
# Run with nodemon
|
|
61
|
+
npx nodemon server.js
|
|
62
|
+
|
|
63
|
+
# Or add to package.json
|
|
64
|
+
npm run dev
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**With TypeScript:**
|
|
68
|
+
```bash
|
|
69
|
+
# Install dependencies
|
|
70
|
+
npm install -D typescript @types/express @types/node ts-node nodemon
|
|
71
|
+
|
|
72
|
+
# Run with nodemon + ts-node
|
|
73
|
+
npx nodemon --exec ts-node src/server.ts
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### With PM2 (Production)
|
|
77
|
+
|
|
78
|
+
**Basic PM2 Start:**
|
|
79
|
+
```bash
|
|
80
|
+
# Start Express with PM2
|
|
81
|
+
pm2 start server.js --name "express-app"
|
|
82
|
+
|
|
83
|
+
# With environment variables
|
|
84
|
+
pm2 start server.js --name "express-app" --env production
|
|
85
|
+
|
|
86
|
+
# Cluster mode (multiple instances)
|
|
87
|
+
pm2 start server.js -i max
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**Watch Mode (Use Carefully):**
|
|
91
|
+
```bash
|
|
92
|
+
# PM2 with watch (development only, not recommended)
|
|
93
|
+
pm2 start server.js --watch --ignore-watch="node_modules"
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### With Docker
|
|
97
|
+
|
|
98
|
+
**Development Dockerfile:**
|
|
99
|
+
```dockerfile
|
|
100
|
+
FROM node:18-alpine
|
|
101
|
+
|
|
102
|
+
WORKDIR /app
|
|
103
|
+
|
|
104
|
+
COPY package*.json ./
|
|
105
|
+
RUN npm install
|
|
106
|
+
|
|
107
|
+
COPY . .
|
|
108
|
+
|
|
109
|
+
EXPOSE 3000
|
|
110
|
+
|
|
111
|
+
CMD ["npm", "run", "dev"]
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
**Production Dockerfile:**
|
|
115
|
+
```dockerfile
|
|
116
|
+
FROM node:18-alpine
|
|
117
|
+
|
|
118
|
+
WORKDIR /app
|
|
119
|
+
|
|
120
|
+
COPY package*.json ./
|
|
121
|
+
RUN npm ci --only=production
|
|
122
|
+
|
|
123
|
+
COPY . .
|
|
124
|
+
|
|
125
|
+
EXPOSE 3000
|
|
126
|
+
|
|
127
|
+
CMD ["node", "server.js"]
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
**Docker Compose:**
|
|
131
|
+
```yaml
|
|
132
|
+
version: '3.8'
|
|
133
|
+
|
|
134
|
+
services:
|
|
135
|
+
express:
|
|
136
|
+
build: .
|
|
137
|
+
ports:
|
|
138
|
+
- "3000:3000"
|
|
139
|
+
volumes:
|
|
140
|
+
- .:/app
|
|
141
|
+
- /app/node_modules
|
|
142
|
+
environment:
|
|
143
|
+
- NODE_ENV=development
|
|
144
|
+
- PORT=3000
|
|
145
|
+
command: npm run dev
|
|
146
|
+
|
|
147
|
+
redis:
|
|
148
|
+
image: redis:7-alpine
|
|
149
|
+
ports:
|
|
150
|
+
- "6379:6379"
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Configuration Patterns
|
|
154
|
+
|
|
155
|
+
### Nodemon Configuration
|
|
156
|
+
|
|
157
|
+
**nodemon.json:**
|
|
158
|
+
```json
|
|
159
|
+
{
|
|
160
|
+
"watch": ["src", "config"],
|
|
161
|
+
"ext": "js,json,ts",
|
|
162
|
+
"ignore": ["src/**/*.test.js", "node_modules"],
|
|
163
|
+
"exec": "node server.js",
|
|
164
|
+
"env": {
|
|
165
|
+
"NODE_ENV": "development"
|
|
166
|
+
},
|
|
167
|
+
"delay": 1000,
|
|
168
|
+
"verbose": true,
|
|
169
|
+
"restartable": "rs"
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
**Package.json Scripts:**
|
|
174
|
+
```json
|
|
175
|
+
{
|
|
176
|
+
"scripts": {
|
|
177
|
+
"dev": "nodemon server.js",
|
|
178
|
+
"dev:debug": "nodemon --inspect server.js",
|
|
179
|
+
"dev:ts": "nodemon --exec ts-node src/server.ts",
|
|
180
|
+
"start": "node server.js",
|
|
181
|
+
"start:prod": "NODE_ENV=production node server.js"
|
|
182
|
+
},
|
|
183
|
+
"devDependencies": {
|
|
184
|
+
"nodemon": "^3.0.1"
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
**Advanced Nodemon Configuration:**
|
|
190
|
+
```json
|
|
191
|
+
{
|
|
192
|
+
"watch": ["src"],
|
|
193
|
+
"ext": "js,json,graphql",
|
|
194
|
+
"ignore": [
|
|
195
|
+
"src/**/*.test.js",
|
|
196
|
+
"src/**/*.spec.js",
|
|
197
|
+
"node_modules/**/*",
|
|
198
|
+
"dist/**/*"
|
|
199
|
+
],
|
|
200
|
+
"exec": "node --trace-warnings server.js",
|
|
201
|
+
"env": {
|
|
202
|
+
"NODE_ENV": "development",
|
|
203
|
+
"DEBUG": "express:*"
|
|
204
|
+
},
|
|
205
|
+
"events": {
|
|
206
|
+
"restart": "echo 'App restarted due to file change'"
|
|
207
|
+
},
|
|
208
|
+
"delay": 2000,
|
|
209
|
+
"signal": "SIGTERM",
|
|
210
|
+
"verbose": false
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### PM2 Clustering for Production
|
|
215
|
+
|
|
216
|
+
**Ecosystem Configuration:**
|
|
217
|
+
```javascript
|
|
218
|
+
// ecosystem.config.js
|
|
219
|
+
module.exports = {
|
|
220
|
+
apps: [{
|
|
221
|
+
name: 'express-app',
|
|
222
|
+
script: './server.js',
|
|
223
|
+
instances: 'max', // Use all CPU cores
|
|
224
|
+
exec_mode: 'cluster',
|
|
225
|
+
|
|
226
|
+
// Environment variables
|
|
227
|
+
env: {
|
|
228
|
+
NODE_ENV: 'development',
|
|
229
|
+
PORT: 3000
|
|
230
|
+
},
|
|
231
|
+
env_production: {
|
|
232
|
+
NODE_ENV: 'production',
|
|
233
|
+
PORT: 8080
|
|
234
|
+
},
|
|
235
|
+
|
|
236
|
+
// Restart policies
|
|
237
|
+
autorestart: true,
|
|
238
|
+
max_restarts: 10,
|
|
239
|
+
min_uptime: '10s',
|
|
240
|
+
max_memory_restart: '500M',
|
|
241
|
+
|
|
242
|
+
// Watch mode (development only, use nodemon instead)
|
|
243
|
+
watch: false,
|
|
244
|
+
|
|
245
|
+
// Logging
|
|
246
|
+
error_file: './logs/pm2-error.log',
|
|
247
|
+
out_file: './logs/pm2-out.log',
|
|
248
|
+
log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
|
|
249
|
+
merge_logs: true,
|
|
250
|
+
|
|
251
|
+
// Graceful shutdown
|
|
252
|
+
kill_timeout: 5000,
|
|
253
|
+
wait_ready: true,
|
|
254
|
+
listen_timeout: 10000,
|
|
255
|
+
}]
|
|
256
|
+
};
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
**Start PM2 Cluster:**
|
|
260
|
+
```bash
|
|
261
|
+
# Start cluster with config
|
|
262
|
+
pm2 start ecosystem.config.js --env production
|
|
263
|
+
|
|
264
|
+
# Monitor cluster
|
|
265
|
+
pm2 monit
|
|
266
|
+
|
|
267
|
+
# View cluster status
|
|
268
|
+
pm2 list
|
|
269
|
+
|
|
270
|
+
# Reload without downtime
|
|
271
|
+
pm2 reload express-app
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
**Manual Clustering Configuration:**
|
|
275
|
+
```bash
|
|
276
|
+
# Start with 4 instances
|
|
277
|
+
pm2 start server.js -i 4
|
|
278
|
+
|
|
279
|
+
# Use all CPU cores
|
|
280
|
+
pm2 start server.js -i max
|
|
281
|
+
|
|
282
|
+
# Scale up/down
|
|
283
|
+
pm2 scale express-app 8
|
|
284
|
+
pm2 scale express-app +2
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### Nodemon vs PM2 Comparison
|
|
288
|
+
|
|
289
|
+
**Nodemon (Development Recommended):**
|
|
290
|
+
|
|
291
|
+
Pros:
|
|
292
|
+
- Designed for development
|
|
293
|
+
- Fast restarts on file changes
|
|
294
|
+
- Simple configuration
|
|
295
|
+
- Better developer experience
|
|
296
|
+
- Automatic detection of file changes
|
|
297
|
+
- No cluster complexity in dev
|
|
298
|
+
|
|
299
|
+
Cons:
|
|
300
|
+
- Single instance only
|
|
301
|
+
- Not for production
|
|
302
|
+
- No advanced process management
|
|
303
|
+
|
|
304
|
+
**Use Nodemon when:**
|
|
305
|
+
- Developing locally
|
|
306
|
+
- Need fast feedback on changes
|
|
307
|
+
- Testing and debugging
|
|
308
|
+
- Single developer workflow
|
|
309
|
+
|
|
310
|
+
**PM2 (Production Recommended):**
|
|
311
|
+
|
|
312
|
+
Pros:
|
|
313
|
+
- Multi-instance clustering
|
|
314
|
+
- Zero-downtime reload
|
|
315
|
+
- Process monitoring
|
|
316
|
+
- Automatic restarts
|
|
317
|
+
- Log management
|
|
318
|
+
- Load balancing
|
|
319
|
+
- Production-grade features
|
|
320
|
+
|
|
321
|
+
Cons:
|
|
322
|
+
- Watch mode can be unreliable
|
|
323
|
+
- More complex configuration
|
|
324
|
+
- Slower restarts than Nodemon
|
|
325
|
+
- Overkill for development
|
|
326
|
+
|
|
327
|
+
**Use PM2 when:**
|
|
328
|
+
- Production deployment
|
|
329
|
+
- Need multiple instances
|
|
330
|
+
- Zero-downtime requirements
|
|
331
|
+
- System-level process management
|
|
332
|
+
- Advanced monitoring needed
|
|
333
|
+
|
|
334
|
+
**Recommendation:**
|
|
335
|
+
- Development: Use Nodemon
|
|
336
|
+
- Production: Use PM2 (no watch mode)
|
|
337
|
+
|
|
338
|
+
### Graceful Shutdown Implementation
|
|
339
|
+
|
|
340
|
+
**Basic Graceful Shutdown:**
|
|
341
|
+
```javascript
|
|
342
|
+
const express = require('express');
|
|
343
|
+
const app = express();
|
|
344
|
+
const PORT = process.env.PORT || 3000;
|
|
345
|
+
|
|
346
|
+
app.get('/', (req, res) => {
|
|
347
|
+
res.json({ status: 'ok' });
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
const server = app.listen(PORT, () => {
|
|
351
|
+
console.log(`Server running on port ${PORT}`);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
// Graceful shutdown handler
|
|
355
|
+
function gracefulShutdown(signal) {
|
|
356
|
+
console.log(`${signal} received, starting graceful shutdown`);
|
|
357
|
+
|
|
358
|
+
server.close(() => {
|
|
359
|
+
console.log('HTTP server closed');
|
|
360
|
+
|
|
361
|
+
// Close database connections
|
|
362
|
+
// db.close();
|
|
363
|
+
|
|
364
|
+
// Close other resources
|
|
365
|
+
// redis.quit();
|
|
366
|
+
|
|
367
|
+
console.log('All connections closed, exiting');
|
|
368
|
+
process.exit(0);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
// Force exit after timeout
|
|
372
|
+
setTimeout(() => {
|
|
373
|
+
console.error('Forcing shutdown after timeout');
|
|
374
|
+
process.exit(1);
|
|
375
|
+
}, 10000);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Listen for termination signals
|
|
379
|
+
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
|
|
380
|
+
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
**Advanced Graceful Shutdown with Cleanup:**
|
|
384
|
+
```javascript
|
|
385
|
+
const express = require('express');
|
|
386
|
+
const mongoose = require('mongoose');
|
|
387
|
+
const redis = require('redis');
|
|
388
|
+
|
|
389
|
+
const app = express();
|
|
390
|
+
const PORT = process.env.PORT || 3000;
|
|
391
|
+
|
|
392
|
+
// Setup
|
|
393
|
+
const redisClient = redis.createClient();
|
|
394
|
+
mongoose.connect(process.env.MONGODB_URI);
|
|
395
|
+
|
|
396
|
+
let isShuttingDown = false;
|
|
397
|
+
|
|
398
|
+
// Health check endpoint
|
|
399
|
+
app.get('/health', (req, res) => {
|
|
400
|
+
if (isShuttingDown) {
|
|
401
|
+
res.status(503).json({ status: 'shutting down' });
|
|
402
|
+
} else {
|
|
403
|
+
res.json({ status: 'ok' });
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
const server = app.listen(PORT, () => {
|
|
408
|
+
console.log(`Server running on port ${PORT}`);
|
|
409
|
+
|
|
410
|
+
// Signal to PM2 that app is ready
|
|
411
|
+
if (process.send) {
|
|
412
|
+
process.send('ready');
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
// Graceful shutdown
|
|
417
|
+
async function gracefulShutdown(signal) {
|
|
418
|
+
if (isShuttingDown) return;
|
|
419
|
+
|
|
420
|
+
console.log(`${signal} received, starting graceful shutdown`);
|
|
421
|
+
isShuttingDown = true;
|
|
422
|
+
|
|
423
|
+
// Stop accepting new connections
|
|
424
|
+
server.close(async () => {
|
|
425
|
+
console.log('HTTP server closed');
|
|
426
|
+
|
|
427
|
+
try {
|
|
428
|
+
// Close database connections
|
|
429
|
+
await mongoose.connection.close();
|
|
430
|
+
console.log('MongoDB connection closed');
|
|
431
|
+
|
|
432
|
+
// Close Redis connection
|
|
433
|
+
await redisClient.quit();
|
|
434
|
+
console.log('Redis connection closed');
|
|
435
|
+
|
|
436
|
+
console.log('Graceful shutdown completed');
|
|
437
|
+
process.exit(0);
|
|
438
|
+
} catch (error) {
|
|
439
|
+
console.error('Error during shutdown:', error);
|
|
440
|
+
process.exit(1);
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
// Force exit after 30 seconds
|
|
445
|
+
setTimeout(() => {
|
|
446
|
+
console.error('Forcing shutdown after timeout');
|
|
447
|
+
process.exit(1);
|
|
448
|
+
}, 30000);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
|
|
452
|
+
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
|
|
453
|
+
|
|
454
|
+
// Handle uncaught errors
|
|
455
|
+
process.on('uncaughtException', (error) => {
|
|
456
|
+
console.error('Uncaught exception:', error);
|
|
457
|
+
gracefulShutdown('uncaughtException');
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
461
|
+
console.error('Unhandled rejection at:', promise, 'reason:', reason);
|
|
462
|
+
gracefulShutdown('unhandledRejection');
|
|
463
|
+
});
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
**PM2 Integration:**
|
|
467
|
+
```javascript
|
|
468
|
+
// Send ready signal to PM2
|
|
469
|
+
server.listen(PORT, () => {
|
|
470
|
+
console.log(`Server listening on port ${PORT}`);
|
|
471
|
+
|
|
472
|
+
if (process.send) {
|
|
473
|
+
process.send('ready');
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
// Handle PM2 shutdown message
|
|
478
|
+
process.on('message', (msg) => {
|
|
479
|
+
if (msg === 'shutdown') {
|
|
480
|
+
gracefulShutdown('PM2 shutdown message');
|
|
481
|
+
}
|
|
482
|
+
});
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
### Zero-Downtime Reload
|
|
486
|
+
|
|
487
|
+
**PM2 Reload (Graceful):**
|
|
488
|
+
```bash
|
|
489
|
+
# Reload all instances gracefully
|
|
490
|
+
pm2 reload express-app
|
|
491
|
+
|
|
492
|
+
# Reload with delay between instances
|
|
493
|
+
pm2 reload express-app --update-env
|
|
494
|
+
|
|
495
|
+
# Force restart (not graceful)
|
|
496
|
+
pm2 restart express-app
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
**Ecosystem Config for Zero-Downtime:**
|
|
500
|
+
```javascript
|
|
501
|
+
module.exports = {
|
|
502
|
+
apps: [{
|
|
503
|
+
name: 'express-app',
|
|
504
|
+
script: './server.js',
|
|
505
|
+
instances: 4,
|
|
506
|
+
exec_mode: 'cluster',
|
|
507
|
+
|
|
508
|
+
// Zero-downtime reload settings
|
|
509
|
+
wait_ready: true, // Wait for ready signal
|
|
510
|
+
listen_timeout: 10000, // Timeout for ready signal
|
|
511
|
+
kill_timeout: 5000, // Time to wait for graceful shutdown
|
|
512
|
+
|
|
513
|
+
// Restart behavior
|
|
514
|
+
autorestart: true,
|
|
515
|
+
max_restarts: 10,
|
|
516
|
+
min_uptime: '10s',
|
|
517
|
+
}]
|
|
518
|
+
};
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
**Application Code for Zero-Downtime:**
|
|
522
|
+
```javascript
|
|
523
|
+
const express = require('express');
|
|
524
|
+
const app = express();
|
|
525
|
+
const PORT = process.env.PORT || 3000;
|
|
526
|
+
|
|
527
|
+
const server = app.listen(PORT, () => {
|
|
528
|
+
console.log(`Server running on port ${PORT}`);
|
|
529
|
+
|
|
530
|
+
// Signal PM2 that app is ready
|
|
531
|
+
if (process.send) {
|
|
532
|
+
process.send('ready');
|
|
533
|
+
}
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
// Graceful shutdown for PM2 reload
|
|
537
|
+
process.on('SIGINT', () => {
|
|
538
|
+
console.log('SIGINT received, closing server');
|
|
539
|
+
|
|
540
|
+
server.close(() => {
|
|
541
|
+
console.log('Server closed');
|
|
542
|
+
process.exit(0);
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
// Timeout fallback
|
|
546
|
+
setTimeout(() => process.exit(1), 10000);
|
|
547
|
+
});
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
**Deployment Process:**
|
|
551
|
+
```bash
|
|
552
|
+
# Deploy new code
|
|
553
|
+
git pull origin main
|
|
554
|
+
npm install --production
|
|
555
|
+
|
|
556
|
+
# Reload without downtime
|
|
557
|
+
pm2 reload ecosystem.config.js --env production
|
|
558
|
+
|
|
559
|
+
# Verify instances
|
|
560
|
+
pm2 list
|
|
561
|
+
pm2 logs express-app --lines 50
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
### Multiple Instance Coordination
|
|
565
|
+
|
|
566
|
+
**Shared State with Redis:**
|
|
567
|
+
```javascript
|
|
568
|
+
const express = require('express');
|
|
569
|
+
const redis = require('redis');
|
|
570
|
+
const session = require('express-session');
|
|
571
|
+
const RedisStore = require('connect-redis').default;
|
|
572
|
+
|
|
573
|
+
const app = express();
|
|
574
|
+
|
|
575
|
+
// Redis client
|
|
576
|
+
const redisClient = redis.createClient({
|
|
577
|
+
host: process.env.REDIS_HOST || 'localhost',
|
|
578
|
+
port: process.env.REDIS_PORT || 6379,
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
redisClient.on('error', (err) => console.error('Redis error:', err));
|
|
582
|
+
|
|
583
|
+
// Session store with Redis
|
|
584
|
+
app.use(session({
|
|
585
|
+
store: new RedisStore({ client: redisClient }),
|
|
586
|
+
secret: process.env.SESSION_SECRET,
|
|
587
|
+
resave: false,
|
|
588
|
+
saveUninitialized: false,
|
|
589
|
+
cookie: {
|
|
590
|
+
secure: process.env.NODE_ENV === 'production',
|
|
591
|
+
maxAge: 1000 * 60 * 60 * 24, // 24 hours
|
|
592
|
+
},
|
|
593
|
+
}));
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
**Cluster-Safe In-Memory Caching:**
|
|
597
|
+
```javascript
|
|
598
|
+
// DON'T: In-memory cache (not shared across instances)
|
|
599
|
+
const cache = {};
|
|
600
|
+
|
|
601
|
+
app.get('/data/:id', (req, res) => {
|
|
602
|
+
const cached = cache[req.params.id]; // Different per instance!
|
|
603
|
+
if (cached) return res.json(cached);
|
|
604
|
+
|
|
605
|
+
// Fetch and cache
|
|
606
|
+
const data = fetchData(req.params.id);
|
|
607
|
+
cache[req.params.id] = data;
|
|
608
|
+
res.json(data);
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
// DO: Redis cache (shared across instances)
|
|
612
|
+
app.get('/data/:id', async (req, res) => {
|
|
613
|
+
const cached = await redisClient.get(`data:${req.params.id}`);
|
|
614
|
+
if (cached) return res.json(JSON.parse(cached));
|
|
615
|
+
|
|
616
|
+
// Fetch and cache in Redis
|
|
617
|
+
const data = await fetchData(req.params.id);
|
|
618
|
+
await redisClient.setEx(`data:${req.params.id}`, 3600, JSON.stringify(data));
|
|
619
|
+
res.json(data);
|
|
620
|
+
});
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
**Worker Coordination:**
|
|
624
|
+
```javascript
|
|
625
|
+
// Use Redis for distributed locks
|
|
626
|
+
const Redlock = require('redlock');
|
|
627
|
+
|
|
628
|
+
const redlock = new Redlock([redisClient], {
|
|
629
|
+
retryCount: 10,
|
|
630
|
+
retryDelay: 200,
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
app.post('/process-job', async (req, res) => {
|
|
634
|
+
const lock = await redlock.acquire(['locks:process-job'], 5000);
|
|
635
|
+
|
|
636
|
+
try {
|
|
637
|
+
// Only one instance processes this at a time
|
|
638
|
+
await processJob(req.body);
|
|
639
|
+
res.json({ success: true });
|
|
640
|
+
} finally {
|
|
641
|
+
await lock.release();
|
|
642
|
+
}
|
|
643
|
+
});
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
**Socket.IO with PM2 Cluster:**
|
|
647
|
+
```javascript
|
|
648
|
+
const express = require('express');
|
|
649
|
+
const http = require('http');
|
|
650
|
+
const socketIO = require('socket.io');
|
|
651
|
+
const redis = require('redis');
|
|
652
|
+
const { createAdapter } = require('@socket.io/redis-adapter');
|
|
653
|
+
|
|
654
|
+
const app = express();
|
|
655
|
+
const server = http.createServer(app);
|
|
656
|
+
const io = socketIO(server);
|
|
657
|
+
|
|
658
|
+
// Redis adapter for Socket.IO clustering
|
|
659
|
+
const pubClient = redis.createClient();
|
|
660
|
+
const subClient = pubClient.duplicate();
|
|
661
|
+
|
|
662
|
+
io.adapter(createAdapter(pubClient, subClient));
|
|
663
|
+
|
|
664
|
+
io.on('connection', (socket) => {
|
|
665
|
+
console.log('Client connected');
|
|
666
|
+
|
|
667
|
+
socket.on('message', (data) => {
|
|
668
|
+
// Broadcast across all instances
|
|
669
|
+
io.emit('message', data);
|
|
670
|
+
});
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
server.listen(3000);
|
|
674
|
+
```
|
|
675
|
+
|
|
676
|
+
## Framework-Specific Best Practices
|
|
677
|
+
|
|
678
|
+
### Middleware Organization
|
|
679
|
+
|
|
680
|
+
**Proper Middleware Order:**
|
|
681
|
+
```javascript
|
|
682
|
+
const express = require('express');
|
|
683
|
+
const helmet = require('helmet');
|
|
684
|
+
const cors = require('cors');
|
|
685
|
+
const compression = require('compression');
|
|
686
|
+
const morgan = require('morgan');
|
|
687
|
+
|
|
688
|
+
const app = express();
|
|
689
|
+
|
|
690
|
+
// Security middleware (first)
|
|
691
|
+
app.use(helmet());
|
|
692
|
+
app.use(cors({
|
|
693
|
+
origin: process.env.ALLOWED_ORIGINS?.split(',') || '*',
|
|
694
|
+
credentials: true,
|
|
695
|
+
}));
|
|
696
|
+
|
|
697
|
+
// Request parsing
|
|
698
|
+
app.use(express.json({ limit: '10mb' }));
|
|
699
|
+
app.use(express.urlencoded({ extended: true }));
|
|
700
|
+
|
|
701
|
+
// Compression
|
|
702
|
+
app.use(compression());
|
|
703
|
+
|
|
704
|
+
// Logging
|
|
705
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
706
|
+
app.use(morgan('dev'));
|
|
707
|
+
} else {
|
|
708
|
+
app.use(morgan('combined'));
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// Routes
|
|
712
|
+
app.use('/api/users', require('./routes/users'));
|
|
713
|
+
app.use('/api/posts', require('./routes/posts'));
|
|
714
|
+
|
|
715
|
+
// Error handling (last)
|
|
716
|
+
app.use((err, req, res, next) => {
|
|
717
|
+
console.error(err.stack);
|
|
718
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
719
|
+
});
|
|
720
|
+
```
|
|
721
|
+
|
|
722
|
+
### Error Handling Patterns
|
|
723
|
+
|
|
724
|
+
**Centralized Error Handler:**
|
|
725
|
+
```javascript
|
|
726
|
+
// errors/AppError.js
|
|
727
|
+
class AppError extends Error {
|
|
728
|
+
constructor(message, statusCode) {
|
|
729
|
+
super(message);
|
|
730
|
+
this.statusCode = statusCode;
|
|
731
|
+
this.isOperational = true;
|
|
732
|
+
Error.captureStackTrace(this, this.constructor);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// middleware/errorHandler.js
|
|
737
|
+
function errorHandler(err, req, res, next) {
|
|
738
|
+
err.statusCode = err.statusCode || 500;
|
|
739
|
+
err.status = err.status || 'error';
|
|
740
|
+
|
|
741
|
+
if (process.env.NODE_ENV === 'development') {
|
|
742
|
+
res.status(err.statusCode).json({
|
|
743
|
+
status: err.status,
|
|
744
|
+
error: err,
|
|
745
|
+
message: err.message,
|
|
746
|
+
stack: err.stack,
|
|
747
|
+
});
|
|
748
|
+
} else {
|
|
749
|
+
// Production: don't leak error details
|
|
750
|
+
if (err.isOperational) {
|
|
751
|
+
res.status(err.statusCode).json({
|
|
752
|
+
status: err.status,
|
|
753
|
+
message: err.message,
|
|
754
|
+
});
|
|
755
|
+
} else {
|
|
756
|
+
console.error('ERROR:', err);
|
|
757
|
+
res.status(500).json({
|
|
758
|
+
status: 'error',
|
|
759
|
+
message: 'Something went wrong',
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// server.js
|
|
766
|
+
app.use(errorHandler);
|
|
767
|
+
```
|
|
768
|
+
|
|
769
|
+
**Async Error Wrapper:**
|
|
770
|
+
```javascript
|
|
771
|
+
// utils/catchAsync.js
|
|
772
|
+
const catchAsync = (fn) => {
|
|
773
|
+
return (req, res, next) => {
|
|
774
|
+
fn(req, res, next).catch(next);
|
|
775
|
+
};
|
|
776
|
+
};
|
|
777
|
+
|
|
778
|
+
// Usage
|
|
779
|
+
const getUser = catchAsync(async (req, res) => {
|
|
780
|
+
const user = await User.findById(req.params.id);
|
|
781
|
+
if (!user) throw new AppError('User not found', 404);
|
|
782
|
+
res.json({ user });
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
app.get('/users/:id', getUser);
|
|
786
|
+
```
|
|
787
|
+
|
|
788
|
+
### Environment Configuration
|
|
789
|
+
|
|
790
|
+
**Configuration Management:**
|
|
791
|
+
```javascript
|
|
792
|
+
// config/index.js
|
|
793
|
+
require('dotenv').config();
|
|
794
|
+
|
|
795
|
+
module.exports = {
|
|
796
|
+
env: process.env.NODE_ENV || 'development',
|
|
797
|
+
port: parseInt(process.env.PORT, 10) || 3000,
|
|
798
|
+
database: {
|
|
799
|
+
uri: process.env.DATABASE_URL,
|
|
800
|
+
options: {
|
|
801
|
+
useNewUrlParser: true,
|
|
802
|
+
useUnifiedTopology: true,
|
|
803
|
+
},
|
|
804
|
+
},
|
|
805
|
+
redis: {
|
|
806
|
+
host: process.env.REDIS_HOST || 'localhost',
|
|
807
|
+
port: parseInt(process.env.REDIS_PORT, 10) || 6379,
|
|
808
|
+
},
|
|
809
|
+
jwt: {
|
|
810
|
+
secret: process.env.JWT_SECRET,
|
|
811
|
+
expiresIn: process.env.JWT_EXPIRES_IN || '7d',
|
|
812
|
+
},
|
|
813
|
+
cors: {
|
|
814
|
+
origin: process.env.ALLOWED_ORIGINS?.split(',') || '*',
|
|
815
|
+
},
|
|
816
|
+
};
|
|
817
|
+
```
|
|
818
|
+
|
|
819
|
+
### Logging Best Practices
|
|
820
|
+
|
|
821
|
+
**Structured Logging:**
|
|
822
|
+
```javascript
|
|
823
|
+
const winston = require('winston');
|
|
824
|
+
|
|
825
|
+
const logger = winston.createLogger({
|
|
826
|
+
level: process.env.LOG_LEVEL || 'info',
|
|
827
|
+
format: winston.format.combine(
|
|
828
|
+
winston.format.timestamp(),
|
|
829
|
+
winston.format.errors({ stack: true }),
|
|
830
|
+
winston.format.json()
|
|
831
|
+
),
|
|
832
|
+
transports: [
|
|
833
|
+
new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),
|
|
834
|
+
new winston.transports.File({ filename: 'logs/combined.log' }),
|
|
835
|
+
],
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
839
|
+
logger.add(new winston.transports.Console({
|
|
840
|
+
format: winston.format.simple(),
|
|
841
|
+
}));
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
// Usage
|
|
845
|
+
logger.info('Server started', { port: 3000 });
|
|
846
|
+
logger.error('Database error', { error: err.message });
|
|
847
|
+
```
|
|
848
|
+
|
|
849
|
+
## Common Problems & Solutions
|
|
850
|
+
|
|
851
|
+
### Problem 1: Port Already in Use
|
|
852
|
+
|
|
853
|
+
**Symptoms:**
|
|
854
|
+
```
|
|
855
|
+
Error: listen EADDRINUSE: address already in use :::3000
|
|
856
|
+
```
|
|
857
|
+
|
|
858
|
+
**Root Cause:**
|
|
859
|
+
Another process (previous Express instance, other server, or zombie process) is using port 3000.
|
|
860
|
+
|
|
861
|
+
**Solution:**
|
|
862
|
+
|
|
863
|
+
**Option A: Kill Process**
|
|
864
|
+
```bash
|
|
865
|
+
# macOS/Linux
|
|
866
|
+
lsof -ti:3000 | xargs kill -9
|
|
867
|
+
|
|
868
|
+
# Alternative
|
|
869
|
+
fuser -k 3000/tcp
|
|
870
|
+
|
|
871
|
+
# Windows
|
|
872
|
+
netstat -ano | findstr :3000
|
|
873
|
+
taskkill /PID <PID> /F
|
|
874
|
+
```
|
|
875
|
+
|
|
876
|
+
**Option B: Use Different Port**
|
|
877
|
+
```bash
|
|
878
|
+
PORT=3001 npm run dev
|
|
879
|
+
```
|
|
880
|
+
|
|
881
|
+
**Option C: Dynamic Port Assignment**
|
|
882
|
+
```javascript
|
|
883
|
+
const express = require('express');
|
|
884
|
+
const app = express();
|
|
885
|
+
|
|
886
|
+
const PORT = process.env.PORT || 3000;
|
|
887
|
+
|
|
888
|
+
const server = app.listen(PORT, () => {
|
|
889
|
+
console.log(`Server running on port ${server.address().port}`);
|
|
890
|
+
});
|
|
891
|
+
|
|
892
|
+
// Or find available port
|
|
893
|
+
const portfinder = require('portfinder');
|
|
894
|
+
|
|
895
|
+
portfinder.getPort((err, port) => {
|
|
896
|
+
if (err) throw err;
|
|
897
|
+
app.listen(port, () => {
|
|
898
|
+
console.log(`Server running on port ${port}`);
|
|
899
|
+
});
|
|
900
|
+
});
|
|
901
|
+
```
|
|
902
|
+
|
|
903
|
+
**Option D: Cleanup Script**
|
|
904
|
+
```json
|
|
905
|
+
{
|
|
906
|
+
"scripts": {
|
|
907
|
+
"predev": "kill-port 3000 || true",
|
|
908
|
+
"dev": "nodemon server.js"
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
```
|
|
912
|
+
|
|
913
|
+
### Problem 2: Nodemon Not Restarting on Changes
|
|
914
|
+
|
|
915
|
+
**Symptoms:**
|
|
916
|
+
- File changes don't trigger restart
|
|
917
|
+
- Need to manually stop and start server
|
|
918
|
+
- Nodemon seems to be running but not watching
|
|
919
|
+
|
|
920
|
+
**Root Cause:**
|
|
921
|
+
Incorrect watch configuration, file permission issues, or unsupported file system (WSL, network drives).
|
|
922
|
+
|
|
923
|
+
**Solution:**
|
|
924
|
+
|
|
925
|
+
**Step 1: Verify Nodemon is Watching**
|
|
926
|
+
```bash
|
|
927
|
+
# Run with verbose output
|
|
928
|
+
npx nodemon --verbose server.js
|
|
929
|
+
```
|
|
930
|
+
|
|
931
|
+
**Step 2: Configure Watch Directories**
|
|
932
|
+
```json
|
|
933
|
+
{
|
|
934
|
+
"watch": ["src", "config"],
|
|
935
|
+
"ext": "js,json",
|
|
936
|
+
"ignore": ["node_modules/**/*", "test/**/*"]
|
|
937
|
+
}
|
|
938
|
+
```
|
|
939
|
+
|
|
940
|
+
**Step 3: Force Polling (WSL/Network Drives)**
|
|
941
|
+
```json
|
|
942
|
+
{
|
|
943
|
+
"watch": ["src"],
|
|
944
|
+
"legacyWatch": true,
|
|
945
|
+
"pollingInterval": 1000
|
|
946
|
+
}
|
|
947
|
+
```
|
|
948
|
+
|
|
949
|
+
**Step 4: Check File Permissions**
|
|
950
|
+
```bash
|
|
951
|
+
# Ensure files are readable
|
|
952
|
+
chmod -R 644 src/**/*.js
|
|
953
|
+
```
|
|
954
|
+
|
|
955
|
+
**Step 5: Increase Delay**
|
|
956
|
+
```json
|
|
957
|
+
{
|
|
958
|
+
"delay": 2000,
|
|
959
|
+
"debounce": 1000
|
|
960
|
+
}
|
|
961
|
+
```
|
|
962
|
+
|
|
963
|
+
**Step 6: Manual Restart**
|
|
964
|
+
```bash
|
|
965
|
+
# Type 'rs' and Enter to manually restart
|
|
966
|
+
npx nodemon server.js
|
|
967
|
+
rs
|
|
968
|
+
```
|
|
969
|
+
|
|
970
|
+
### Problem 3: PM2 Cluster Instances Crashing
|
|
971
|
+
|
|
972
|
+
**Symptoms:**
|
|
973
|
+
```
|
|
974
|
+
[PM2][ERROR] App crashed
|
|
975
|
+
PM2 | App [express-app:0] exited with code [1]
|
|
976
|
+
```
|
|
977
|
+
|
|
978
|
+
**Root Cause:**
|
|
979
|
+
Unhandled errors, memory leaks, or improper graceful shutdown in cluster mode.
|
|
980
|
+
|
|
981
|
+
**Solution:**
|
|
982
|
+
|
|
983
|
+
**Step 1: Check Logs**
|
|
984
|
+
```bash
|
|
985
|
+
pm2 logs express-app --lines 100
|
|
986
|
+
pm2 logs express-app --err
|
|
987
|
+
```
|
|
988
|
+
|
|
989
|
+
**Step 2: Implement Error Handlers**
|
|
990
|
+
```javascript
|
|
991
|
+
// Catch unhandled errors
|
|
992
|
+
process.on('uncaughtException', (error) => {
|
|
993
|
+
console.error('Uncaught exception:', error);
|
|
994
|
+
gracefulShutdown('uncaughtException');
|
|
995
|
+
});
|
|
996
|
+
|
|
997
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
998
|
+
console.error('Unhandled rejection:', reason);
|
|
999
|
+
gracefulShutdown('unhandledRejection');
|
|
1000
|
+
});
|
|
1001
|
+
```
|
|
1002
|
+
|
|
1003
|
+
**Step 3: Add Memory Monitoring**
|
|
1004
|
+
```javascript
|
|
1005
|
+
// ecosystem.config.js
|
|
1006
|
+
module.exports = {
|
|
1007
|
+
apps: [{
|
|
1008
|
+
name: 'express-app',
|
|
1009
|
+
script: './server.js',
|
|
1010
|
+
max_memory_restart: '500M', // Restart on high memory
|
|
1011
|
+
instances: 4,
|
|
1012
|
+
exec_mode: 'cluster',
|
|
1013
|
+
}]
|
|
1014
|
+
};
|
|
1015
|
+
```
|
|
1016
|
+
|
|
1017
|
+
**Step 4: Check Minimum Uptime**
|
|
1018
|
+
```javascript
|
|
1019
|
+
// ecosystem.config.js
|
|
1020
|
+
module.exports = {
|
|
1021
|
+
apps: [{
|
|
1022
|
+
min_uptime: '10s', // App must run 10s to be considered healthy
|
|
1023
|
+
max_restarts: 10, // Max restarts within min_uptime period
|
|
1024
|
+
}]
|
|
1025
|
+
};
|
|
1026
|
+
```
|
|
1027
|
+
|
|
1028
|
+
**Step 5: Test Instance Stability**
|
|
1029
|
+
```bash
|
|
1030
|
+
# Start single instance first
|
|
1031
|
+
pm2 start server.js --name "test" -i 1
|
|
1032
|
+
|
|
1033
|
+
# If stable, scale up
|
|
1034
|
+
pm2 scale test 4
|
|
1035
|
+
```
|
|
1036
|
+
|
|
1037
|
+
### Problem 4: Sessions Not Shared Across Instances
|
|
1038
|
+
|
|
1039
|
+
**Symptoms:**
|
|
1040
|
+
- User logged in but subsequent requests show logged out
|
|
1041
|
+
- Session data inconsistent between requests
|
|
1042
|
+
- Works fine with single instance, breaks with cluster
|
|
1043
|
+
|
|
1044
|
+
**Root Cause:**
|
|
1045
|
+
In-memory session store is not shared across PM2 cluster instances.
|
|
1046
|
+
|
|
1047
|
+
**Solution:**
|
|
1048
|
+
|
|
1049
|
+
**Use Redis for Session Storage:**
|
|
1050
|
+
```javascript
|
|
1051
|
+
const express = require('express');
|
|
1052
|
+
const session = require('express-session');
|
|
1053
|
+
const RedisStore = require('connect-redis').default;
|
|
1054
|
+
const redis = require('redis');
|
|
1055
|
+
|
|
1056
|
+
const app = express();
|
|
1057
|
+
|
|
1058
|
+
// Redis client
|
|
1059
|
+
const redisClient = redis.createClient({
|
|
1060
|
+
host: process.env.REDIS_HOST || 'localhost',
|
|
1061
|
+
port: process.env.REDIS_PORT || 6379,
|
|
1062
|
+
});
|
|
1063
|
+
|
|
1064
|
+
redisClient.on('error', (err) => console.error('Redis error:', err));
|
|
1065
|
+
|
|
1066
|
+
// Session configuration with Redis store
|
|
1067
|
+
app.use(session({
|
|
1068
|
+
store: new RedisStore({ client: redisClient }),
|
|
1069
|
+
secret: process.env.SESSION_SECRET || 'your-secret-key',
|
|
1070
|
+
resave: false,
|
|
1071
|
+
saveUninitialized: false,
|
|
1072
|
+
cookie: {
|
|
1073
|
+
secure: process.env.NODE_ENV === 'production', // HTTPS only in prod
|
|
1074
|
+
httpOnly: true,
|
|
1075
|
+
maxAge: 1000 * 60 * 60 * 24, // 24 hours
|
|
1076
|
+
},
|
|
1077
|
+
}));
|
|
1078
|
+
```
|
|
1079
|
+
|
|
1080
|
+
**Alternative: Use JWT (Stateless):**
|
|
1081
|
+
```javascript
|
|
1082
|
+
const jwt = require('jsonwebtoken');
|
|
1083
|
+
|
|
1084
|
+
// Generate token on login
|
|
1085
|
+
app.post('/login', (req, res) => {
|
|
1086
|
+
// Verify credentials
|
|
1087
|
+
const token = jwt.sign(
|
|
1088
|
+
{ userId: user.id },
|
|
1089
|
+
process.env.JWT_SECRET,
|
|
1090
|
+
{ expiresIn: '7d' }
|
|
1091
|
+
);
|
|
1092
|
+
|
|
1093
|
+
res.json({ token });
|
|
1094
|
+
});
|
|
1095
|
+
|
|
1096
|
+
// Verify token on protected routes
|
|
1097
|
+
function authMiddleware(req, res, next) {
|
|
1098
|
+
const token = req.headers.authorization?.split(' ')[1];
|
|
1099
|
+
|
|
1100
|
+
if (!token) {
|
|
1101
|
+
return res.status(401).json({ error: 'No token provided' });
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
try {
|
|
1105
|
+
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
|
1106
|
+
req.user = decoded;
|
|
1107
|
+
next();
|
|
1108
|
+
} catch (error) {
|
|
1109
|
+
res.status(401).json({ error: 'Invalid token' });
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
app.get('/protected', authMiddleware, (req, res) => {
|
|
1114
|
+
res.json({ data: 'secret data', user: req.user });
|
|
1115
|
+
});
|
|
1116
|
+
```
|
|
1117
|
+
|
|
1118
|
+
### Problem 5: Nodemon vs PM2 Watch Confusion
|
|
1119
|
+
|
|
1120
|
+
**Symptoms:**
|
|
1121
|
+
- Using PM2 watch mode for development
|
|
1122
|
+
- Slow restarts or unexpected behavior
|
|
1123
|
+
- File changes trigger multiple restarts
|
|
1124
|
+
|
|
1125
|
+
**Root Cause:**
|
|
1126
|
+
PM2 watch mode is not designed for active development and has limitations compared to Nodemon.
|
|
1127
|
+
|
|
1128
|
+
**Solution:**
|
|
1129
|
+
|
|
1130
|
+
**Use Nodemon for Development:**
|
|
1131
|
+
```json
|
|
1132
|
+
{
|
|
1133
|
+
"scripts": {
|
|
1134
|
+
"dev": "nodemon server.js",
|
|
1135
|
+
"start": "node server.js",
|
|
1136
|
+
"prod": "pm2 start ecosystem.config.js --env production"
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
```
|
|
1140
|
+
|
|
1141
|
+
**Configure Nodemon Properly:**
|
|
1142
|
+
```json
|
|
1143
|
+
{
|
|
1144
|
+
"watch": ["src"],
|
|
1145
|
+
"ext": "js,json",
|
|
1146
|
+
"ignore": ["src/**/*.test.js"],
|
|
1147
|
+
"exec": "node server.js",
|
|
1148
|
+
"delay": 1000
|
|
1149
|
+
}
|
|
1150
|
+
```
|
|
1151
|
+
|
|
1152
|
+
**Use PM2 for Production (No Watch):**
|
|
1153
|
+
```javascript
|
|
1154
|
+
// ecosystem.config.js
|
|
1155
|
+
module.exports = {
|
|
1156
|
+
apps: [{
|
|
1157
|
+
name: 'express-app',
|
|
1158
|
+
script: './server.js',
|
|
1159
|
+
instances: 'max',
|
|
1160
|
+
exec_mode: 'cluster',
|
|
1161
|
+
watch: false, // NEVER true in production
|
|
1162
|
+
autorestart: true,
|
|
1163
|
+
env_production: {
|
|
1164
|
+
NODE_ENV: 'production'
|
|
1165
|
+
}
|
|
1166
|
+
}]
|
|
1167
|
+
};
|
|
1168
|
+
```
|
|
1169
|
+
|
|
1170
|
+
**Development Workflow:**
|
|
1171
|
+
```bash
|
|
1172
|
+
# Development: Nodemon
|
|
1173
|
+
npm run dev
|
|
1174
|
+
|
|
1175
|
+
# Production: PM2
|
|
1176
|
+
pm2 start ecosystem.config.js --env production
|
|
1177
|
+
```
|
|
1178
|
+
|
|
1179
|
+
## Anti-Patterns
|
|
1180
|
+
|
|
1181
|
+
### What NOT to Do
|
|
1182
|
+
|
|
1183
|
+
**1. Don't Use PM2 Watch for Development**
|
|
1184
|
+
```javascript
|
|
1185
|
+
// WRONG
|
|
1186
|
+
module.exports = {
|
|
1187
|
+
apps: [{
|
|
1188
|
+
name: 'express-dev',
|
|
1189
|
+
script: './server.js',
|
|
1190
|
+
watch: true, // Use nodemon instead
|
|
1191
|
+
}]
|
|
1192
|
+
};
|
|
1193
|
+
```
|
|
1194
|
+
|
|
1195
|
+
Use Nodemon for development, PM2 for production.
|
|
1196
|
+
|
|
1197
|
+
**2. Don't Store Sessions In-Memory with Clustering**
|
|
1198
|
+
```javascript
|
|
1199
|
+
// WRONG with PM2 cluster mode
|
|
1200
|
+
const session = require('express-session');
|
|
1201
|
+
app.use(session({
|
|
1202
|
+
secret: 'secret',
|
|
1203
|
+
resave: false,
|
|
1204
|
+
saveUninitialized: false,
|
|
1205
|
+
// No store specified = in-memory (breaks clustering)
|
|
1206
|
+
}));
|
|
1207
|
+
```
|
|
1208
|
+
|
|
1209
|
+
Use Redis or other shared store for sessions.
|
|
1210
|
+
|
|
1211
|
+
**3. Don't Forget Graceful Shutdown**
|
|
1212
|
+
```javascript
|
|
1213
|
+
// WRONG - abrupt termination
|
|
1214
|
+
app.listen(3000);
|
|
1215
|
+
// No shutdown handlers
|
|
1216
|
+
|
|
1217
|
+
// CORRECT
|
|
1218
|
+
const server = app.listen(3000);
|
|
1219
|
+
process.on('SIGTERM', () => {
|
|
1220
|
+
server.close(() => process.exit(0));
|
|
1221
|
+
});
|
|
1222
|
+
```
|
|
1223
|
+
|
|
1224
|
+
**4. Don't Block the Event Loop**
|
|
1225
|
+
```javascript
|
|
1226
|
+
// WRONG - blocks event loop
|
|
1227
|
+
app.get('/compute', (req, res) => {
|
|
1228
|
+
const result = heavyComputation(); // Synchronous!
|
|
1229
|
+
res.json({ result });
|
|
1230
|
+
});
|
|
1231
|
+
|
|
1232
|
+
// CORRECT - offload to worker
|
|
1233
|
+
const { Worker } = require('worker_threads');
|
|
1234
|
+
app.get('/compute', (req, res) => {
|
|
1235
|
+
const worker = new Worker('./worker.js');
|
|
1236
|
+
worker.on('message', (result) => {
|
|
1237
|
+
res.json({ result });
|
|
1238
|
+
});
|
|
1239
|
+
});
|
|
1240
|
+
```
|
|
1241
|
+
|
|
1242
|
+
**5. Don't Expose Detailed Errors in Production**
|
|
1243
|
+
```javascript
|
|
1244
|
+
// WRONG
|
|
1245
|
+
app.use((err, req, res, next) => {
|
|
1246
|
+
res.status(500).json({ error: err.stack }); // Leaks info
|
|
1247
|
+
});
|
|
1248
|
+
|
|
1249
|
+
// CORRECT
|
|
1250
|
+
app.use((err, req, res, next) => {
|
|
1251
|
+
if (process.env.NODE_ENV === 'production') {
|
|
1252
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
1253
|
+
} else {
|
|
1254
|
+
res.status(500).json({ error: err.message, stack: err.stack });
|
|
1255
|
+
}
|
|
1256
|
+
});
|
|
1257
|
+
```
|
|
1258
|
+
|
|
1259
|
+
**6. Don't Use Synchronous File Operations**
|
|
1260
|
+
```javascript
|
|
1261
|
+
// WRONG
|
|
1262
|
+
const data = fs.readFileSync('./data.json'); // Blocks!
|
|
1263
|
+
|
|
1264
|
+
// CORRECT
|
|
1265
|
+
const data = await fs.promises.readFile('./data.json');
|
|
1266
|
+
```
|
|
1267
|
+
|
|
1268
|
+
**7. Don't Trust User Input**
|
|
1269
|
+
```javascript
|
|
1270
|
+
// WRONG - no validation
|
|
1271
|
+
app.post('/users', (req, res) => {
|
|
1272
|
+
const user = new User(req.body); // Dangerous!
|
|
1273
|
+
user.save();
|
|
1274
|
+
});
|
|
1275
|
+
|
|
1276
|
+
// CORRECT
|
|
1277
|
+
const { body, validationResult } = require('express-validator');
|
|
1278
|
+
|
|
1279
|
+
app.post('/users',
|
|
1280
|
+
body('email').isEmail(),
|
|
1281
|
+
body('name').trim().notEmpty(),
|
|
1282
|
+
(req, res) => {
|
|
1283
|
+
const errors = validationResult(req);
|
|
1284
|
+
if (!errors.isEmpty()) {
|
|
1285
|
+
return res.status(400).json({ errors: errors.array() });
|
|
1286
|
+
}
|
|
1287
|
+
// Process validated input
|
|
1288
|
+
}
|
|
1289
|
+
);
|
|
1290
|
+
```
|
|
1291
|
+
|
|
1292
|
+
## Quick Reference
|
|
1293
|
+
|
|
1294
|
+
### Commands
|
|
1295
|
+
|
|
1296
|
+
```bash
|
|
1297
|
+
# Development with Nodemon
|
|
1298
|
+
npm run dev # Start with nodemon
|
|
1299
|
+
npx nodemon server.js # Direct nodemon
|
|
1300
|
+
npx nodemon --verbose server.js # Verbose output
|
|
1301
|
+
npx nodemon --inspect server.js # Debug mode
|
|
1302
|
+
|
|
1303
|
+
# Production with PM2
|
|
1304
|
+
pm2 start server.js # Start app
|
|
1305
|
+
pm2 start ecosystem.config.js # Start with config
|
|
1306
|
+
pm2 start server.js -i max # Cluster mode (all CPUs)
|
|
1307
|
+
pm2 reload express-app # Zero-downtime reload
|
|
1308
|
+
pm2 restart express-app # Restart
|
|
1309
|
+
pm2 stop express-app # Stop
|
|
1310
|
+
pm2 delete express-app # Remove from PM2
|
|
1311
|
+
pm2 logs express-app # View logs
|
|
1312
|
+
pm2 monit # Monitor dashboard
|
|
1313
|
+
pm2 list # List processes
|
|
1314
|
+
pm2 save # Save process list
|
|
1315
|
+
pm2 startup # Enable auto-start
|
|
1316
|
+
|
|
1317
|
+
# Process Management
|
|
1318
|
+
lsof -ti:3000 | xargs kill -9 # Kill process on port
|
|
1319
|
+
ps aux | grep node # Find Node processes
|
|
1320
|
+
kill -9 <PID> # Kill specific process
|
|
1321
|
+
|
|
1322
|
+
# Scaling
|
|
1323
|
+
pm2 scale express-app 4 # Set to 4 instances
|
|
1324
|
+
pm2 scale express-app +2 # Add 2 instances
|
|
1325
|
+
```
|
|
1326
|
+
|
|
1327
|
+
### Configuration Templates
|
|
1328
|
+
|
|
1329
|
+
**package.json:**
|
|
1330
|
+
```json
|
|
1331
|
+
{
|
|
1332
|
+
"scripts": {
|
|
1333
|
+
"dev": "nodemon server.js",
|
|
1334
|
+
"dev:debug": "nodemon --inspect server.js",
|
|
1335
|
+
"start": "node server.js",
|
|
1336
|
+
"prod": "pm2 start ecosystem.config.js --env production",
|
|
1337
|
+
"reload": "pm2 reload ecosystem.config.js",
|
|
1338
|
+
"stop": "pm2 stop ecosystem.config.js"
|
|
1339
|
+
},
|
|
1340
|
+
"dependencies": {
|
|
1341
|
+
"express": "^4.18.2",
|
|
1342
|
+
"dotenv": "^16.0.3"
|
|
1343
|
+
},
|
|
1344
|
+
"devDependencies": {
|
|
1345
|
+
"nodemon": "^3.0.1"
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
```
|
|
1349
|
+
|
|
1350
|
+
**nodemon.json:**
|
|
1351
|
+
```json
|
|
1352
|
+
{
|
|
1353
|
+
"watch": ["src"],
|
|
1354
|
+
"ext": "js,json",
|
|
1355
|
+
"ignore": ["src/**/*.test.js"],
|
|
1356
|
+
"exec": "node server.js",
|
|
1357
|
+
"env": {
|
|
1358
|
+
"NODE_ENV": "development"
|
|
1359
|
+
},
|
|
1360
|
+
"delay": 1000
|
|
1361
|
+
}
|
|
1362
|
+
```
|
|
1363
|
+
|
|
1364
|
+
**ecosystem.config.js (Production):**
|
|
1365
|
+
```javascript
|
|
1366
|
+
module.exports = {
|
|
1367
|
+
apps: [{
|
|
1368
|
+
name: 'express-app',
|
|
1369
|
+
script: './server.js',
|
|
1370
|
+
instances: 'max',
|
|
1371
|
+
exec_mode: 'cluster',
|
|
1372
|
+
autorestart: true,
|
|
1373
|
+
watch: false,
|
|
1374
|
+
max_memory_restart: '500M',
|
|
1375
|
+
env_production: {
|
|
1376
|
+
NODE_ENV: 'production',
|
|
1377
|
+
PORT: 8080
|
|
1378
|
+
},
|
|
1379
|
+
error_file: './logs/pm2-error.log',
|
|
1380
|
+
out_file: './logs/pm2-out.log',
|
|
1381
|
+
log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
|
|
1382
|
+
merge_logs: true,
|
|
1383
|
+
kill_timeout: 5000,
|
|
1384
|
+
wait_ready: true,
|
|
1385
|
+
listen_timeout: 10000,
|
|
1386
|
+
}]
|
|
1387
|
+
};
|
|
1388
|
+
```
|
|
1389
|
+
|
|
1390
|
+
**server.js with Graceful Shutdown:**
|
|
1391
|
+
```javascript
|
|
1392
|
+
const express = require('express');
|
|
1393
|
+
const app = express();
|
|
1394
|
+
const PORT = process.env.PORT || 3000;
|
|
1395
|
+
|
|
1396
|
+
app.get('/health', (req, res) => {
|
|
1397
|
+
res.json({ status: 'ok' });
|
|
1398
|
+
});
|
|
1399
|
+
|
|
1400
|
+
const server = app.listen(PORT, () => {
|
|
1401
|
+
console.log(`Server running on port ${PORT}`);
|
|
1402
|
+
if (process.send) process.send('ready');
|
|
1403
|
+
});
|
|
1404
|
+
|
|
1405
|
+
function gracefulShutdown(signal) {
|
|
1406
|
+
console.log(`${signal} received`);
|
|
1407
|
+
server.close(() => {
|
|
1408
|
+
console.log('Server closed');
|
|
1409
|
+
process.exit(0);
|
|
1410
|
+
});
|
|
1411
|
+
setTimeout(() => process.exit(1), 10000);
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
|
|
1415
|
+
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
|
|
1416
|
+
```
|
|
1417
|
+
|
|
1418
|
+
## Related Skills
|
|
1419
|
+
|
|
1420
|
+
- **fastapi-local-dev** - Similar patterns for Python/FastAPI applications
|
|
1421
|
+
- **nextjs-local-dev** - For Next.js with custom Express server
|
|
1422
|
+
- **docker-containerization** - For containerized Express deployments
|
|
1423
|
+
- **systematic-debugging** - For complex debugging scenarios
|
|
1424
|
+
|
|
1425
|
+
---
|
|
1426
|
+
|
|
1427
|
+
**Express Version Compatibility:** This skill covers Express 4.x and PM2 5.x. Most patterns are stable across versions.
|
|
1428
|
+
|
|
1429
|
+
**Last Updated:** 2024 - Reflects current Express and PM2 best practices for development and production deployment.
|