Node.js Best Practices Skill
Overview
This skill ensures clean, maintainable, production-ready Node.js code. It covers Express application structure, PostgreSQL database patterns, error handling, testing, and security.
Project Structure
Organize code into distinct layers:
server/ ├── index.js # App setup, middleware mounting ├── routes/ # HTTP handling only, no business logic ├── services/ # Business logic, database operations ├── data/ # Static data, definitions, constants ├── db/ # Database connection, migrations └── auth/ # Authentication middleware
Routes handle HTTP concerns (parsing requests, sending responses). Services contain business logic and database operations. Data modules export static definitions and pure functions.
Core Principles
- Separation of Concerns
Routes should delegate to services, not contain business logic:
// GOOD: Route delegates to service router.post('/:gameId/action', async (req, res) => { try { const result = await gameService.processAction( req.params.gameId, req.session.userId, req.body ); res.json(result); } catch (error) { res.status(400).json({ error: error.message }); } });
- Consistent Async/Await
Use async/await consistently. Never mix with callbacks:
// GOOD: Clean async/await async function getUser(userId) { const result = await pool.query('SELECT * FROM users WHERE id = $1', [userId]); return result.rows[0] || null; }
- Always Use Parameterized Queries
Never interpolate user input into SQL:
// GOOD: Parameterized (safe) await pool.query('SELECT * FROM users WHERE id = $1', [userId]);
// BAD: SQL injection vulnerability
await pool.query(SELECT * FROM users WHERE id = ${userId});
- Proper Error Handling
Always handle errors - never swallow them:
router.post('/:id/action', async (req, res) => { try { const result = await processAction(req.params.id, req.body); res.json({ success: true, ...result }); } catch (error) { console.error('Action failed:', error); res.status(400).json({ error: error.message }); } });
Database Patterns
See DATABASE.md for complete database patterns including:
-
Connection pooling configuration
-
Transaction handling with proper rollback
-
Row locking with FOR UPDATE
-
N+1 query prevention
Quick Reference
// Connection pool (singleton) const pool = new Pool({ connectionString: process.env.DATABASE_URL, max: 20, idleTimeoutMillis: 30000 });
// Transaction pattern const client = await pool.connect(); try { await client.query('BEGIN'); // ... operations ... await client.query('COMMIT'); } catch (error) { await client.query('ROLLBACK'); throw error; } finally { client.release(); // ALWAYS release }
Testing Patterns
See TESTING.md for complete testing patterns including:
-
Mocking the database pool
-
Testing transaction rollbacks
-
Route testing patterns
Quick Reference
// Mock database at top of test file jest.mock('../../server/db', () => ({ pool: { query: jest.fn(), connect: jest.fn(), on: jest.fn() } }));
// Reset mocks between tests beforeEach(() => { jest.clearAllMocks(); });
// Mock transaction client const mockClient = { query: jest.fn(), release: jest.fn() }; pool.connect.mockResolvedValue(mockClient);
Express Patterns
Middleware Order
app.set('trust proxy', 1); // 1. Proxy settings app.use(express.json()); // 2. Body parsing app.use(sessionMiddleware); // 3. Session app.use(express.static('public')); // 4. Static files app.use('/api', routes); // 5. Routes app.use(errorHandler); // 6. Error handler (last)
Authentication Middleware
function requireAuth(req, res, next) { if (!req.session?.userId) { return res.status(401).json({ error: 'Not authenticated' }); } next(); }
router.use(requireAuth); // Apply to all routes in file
Input Validation
Validate early in route handlers:
router.post('/:gameId/action', async (req, res) => { const { actionType } = req.body; if (!actionType) { return res.status(400).json({ error: 'Action type is required' }); } // Proceed with validated input... });
Security Essentials
Environment Variables
// GOOD: From environment const pool = new Pool({ connectionString: process.env.DATABASE_URL });
// BAD: Hardcoded credentials const pool = new Pool({ connectionString: 'postgres://user:pass@host/db' });
Password Hashing
const bcrypt = require('bcrypt'); const SALT_ROUNDS = 10;
await bcrypt.hash(password, SALT_ROUNDS); // Hash await bcrypt.compare(password, hash); // Verify
Session Configuration
app.use(session({ secret: process.env.SESSION_SECRET, cookie: { secure: process.env.NODE_ENV === 'production', httpOnly: true, maxAge: 24 * 60 * 60 * 1000 } }));
Performance Tips
Parallel Operations
Use Promise.all for independent operations:
const [game, players, state] = await Promise.all([ getGameById(gameId), getGamePlayers(gameId), getGameState(gameId) ]);
Health Checks
Include database status:
app.get('/health', async (req, res) => { const dbHealthy = await db.healthCheck(); res.status(dbHealthy ? 200 : 503).json({ status: dbHealthy ? 'healthy' : 'degraded', database: dbHealthy ? 'connected' : 'disconnected' }); });
When This Skill Activates
Use this skill when:
-
Writing Express routes or middleware
-
Creating service layer functions
-
Working with PostgreSQL queries
-
Implementing database transactions
-
Writing Jest tests for Node.js code
-
Handling errors and edge cases
-
Setting up authentication or sessions