nodejs-best-practices
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
1. 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 });
}
});
2. 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;
}
3. 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}`);
4. 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
More from fil512/upship
ui-design-expert
Expert UI design guidance for polishing game interfaces. Use when reviewing screenshots, recommending CSS improvements, designing color schemes, improving typography, adding visual polish, or making the game look professional. Specializes in board game aesthetics with a steampunk/brass era theme.
12boardgame-design
Board game design workflow for creating fun, balanced games. Use when designing mechanics, balancing factions, analyzing resource economies, validating rules clarity, planning playtests, or iterating on game systems. Applies Eurogame principles and proven design methodology.
11realtime-multiplayer
Real-time multiplayer game networking with Socket.io. Use when implementing WebSocket connections, game state synchronization, room management, reconnection handling, or optimistic updates. Covers latency compensation and conflict resolution.
8svelte
Modern Svelte development for reactive web apps. Use when building Svelte components, managing state with stores, implementing real-time updates via WebSocket, or migrating from vanilla JS. Covers SvelteKit, TypeScript, and integration with Node.js backends.
4game-state
Game state management for turn-based board games. Use when designing state structure, implementing game logic, validating actions, managing phases/turns, or handling complex game rules. Covers reducers, state machines, and undo/redo.
4rulebook-writing
Expert guidance for writing UP SHIP! rulebook content following Euro-style board game best practices. Use when drafting rules sections, reviewing rules clarity, restructuring content, or ensuring consistency. Covers section ordering, terminology, formatting, cross-referencing, and audience design.
4