Node.js Best Practices
Following best practices helps you write code that is maintainable, secure, and performant. This lesson covers essential patterns and guidelines for building production-quality Node.js applications.
Project Structure
Organize your code in a logical, scalable structure:
project/
├── src/
│ ├── config/ # Configuration files
│ ├── controllers/ # Request handlers
│ ├── middleware/ # Express middleware
│ ├── models/ # Data models
│ ├── routes/ # Route definitions
│ ├── services/ # Business logic
│ ├── utils/ # Helper functions
│ └── index.js # Entry point
├── tests/
│ ├── unit/
│ └── integration/
├── .env.example
├── .gitignore
├── package.json
└── README.md
Loading JavaScript Playground...
Coding Standards
Use Consistent Naming
// Variables and functions: camelCase
const userName = 'Alice';
function getUserById(id) {}
// Classes: PascalCase
class UserService {}
class DatabaseConnection {}
// Constants: UPPER_SNAKE_CASE
const MAX_RETRIES = 3;
const API_BASE_URL = 'https://api.example.com';
// File names: kebab-case or camelCase
// user-service.js or userService.js
Prefer const Over let
// Use const by default
const users = [];
const config = { port: 3000 };
// Use let only when reassignment is needed
let count = 0;
count++;
// Never use var
Use Meaningful Names
// Bad
const d = new Date();
const u = users.filter(x => x.a > 18);
// Good
const currentDate = new Date();
const adultUsers = users.filter(user => user.age > 18);
Error Handling Best Practices
Always Handle Errors
// Bad - swallowing errors
try {
await riskyOperation();
} catch (e) {}
// Good - handle or re-throw
try {
await riskyOperation();
} catch (error) {
logger.error('Operation failed:', error);
throw new AppError('Operation failed', 500);
}
Use Async Error Wrapper
// Wrap async route handlers
const asyncHandler = fn => (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
app.get('/users', asyncHandler(async (req, res) => {
const users = await userService.getAll();
res.json(users);
}));
Security Best Practices
1. Validate All Input
function validateEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!email || !emailRegex.test(email)) {
throw new ValidationError('Invalid email format');
}
return email.toLowerCase().trim();
}
function validateUserId(id) {
const parsed = parseInt(id, 10);
if (isNaN(parsed) || parsed <= 0) {
throw new ValidationError('Invalid user ID');
}
return parsed;
}
2. Never Trust User Input
// Bad - SQL injection risk
const query = `SELECT * FROM users WHERE name = '${userName}'`;
// Good - parameterized query
const query = 'SELECT * FROM users WHERE name = $1';
db.query(query, [userName]);
3. Protect Sensitive Data
// Don't log sensitive data
console.log('User:', {
id: user.id,
email: user.email
// Never: password: user.password
});
// Don't expose in responses
res.json({
id: user.id,
name: user.name
// Never include password hash
});
// Use environment variables for secrets
const apiKey = process.env.API_KEY;
Loading JavaScript Exercise...
Performance Best Practices
1. Use Async Operations
// Bad - blocking the event loop
const data = fs.readFileSync('file.txt');
// Good - non-blocking
const data = await fs.promises.readFile('file.txt');
2. Avoid Memory Leaks
// Bad - growing array without limit
const cache = [];
function addToCache(item) {
cache.push(item);
}
// Good - limit cache size
const MAX_CACHE_SIZE = 1000;
function addToCache(item) {
if (cache.length >= MAX_CACHE_SIZE) {
cache.shift(); // Remove oldest
}
cache.push(item);
}
3. Use Streams for Large Data
// Bad - loads entire file into memory
const data = await fs.readFile('huge-file.csv', 'utf8');
processData(data);
// Good - streams data in chunks
const stream = fs.createReadStream('huge-file.csv');
stream.on('data', chunk => processChunk(chunk));
Loading JavaScript Playground...
Dependency Management
1. Keep Dependencies Updated
# Check for outdated packages
npm outdated
# Update packages
npm update
# Check for vulnerabilities
npm audit
# Fix vulnerabilities
npm audit fix
2. Use Exact Versions in Production
{
"dependencies": {
"express": "4.18.2",
"lodash": "4.17.21"
}
}
3. Minimize Dependencies
// Don't add a package for simple operations
// Instead of lodash for just one function
import _ from 'lodash';
_.isEmpty(obj);
// Use native JavaScript
Object.keys(obj).length === 0;
Logging Best Practices
Use Structured Logging
// Bad - unstructured logs
console.log('User ' + user.id + ' logged in');
// Good - structured logging
logger.info('User logged in', {
userId: user.id,
email: user.email,
timestamp: new Date().toISOString(),
ip: req.ip
});
Use Log Levels Appropriately
logger.debug('Detailed debugging info');
logger.info('General information');
logger.warn('Warning - something unexpected');
logger.error('Error - something failed');
Testing Best Practices
1. Write Testable Code
// Bad - hard to test, uses global state
function getUsers() {
return database.query('SELECT * FROM users');
}
// Good - dependency injection, testable
function createUserService(db) {
return {
getUsers() {
return db.query('SELECT * FROM users');
}
};
}
2. Test Edge Cases
describe('divideNumbers', () => {
it('divides two positive numbers', () => {
expect(divide(10, 2)).toBe(5);
});
it('throws on division by zero', () => {
expect(() => divide(10, 0)).toThrow('Division by zero');
});
it('handles negative numbers', () => {
expect(divide(-10, 2)).toBe(-5);
});
it('handles decimal results', () => {
expect(divide(5, 2)).toBe(2.5);
});
});
Documentation Best Practices
Use JSDoc Comments
/**
* Creates a new user in the system.
*
* @param {Object} userData - The user data.
* @param {string} userData.name - The user's full name.
* @param {string} userData.email - The user's email address.
* @param {number} [userData.age] - The user's age (optional).
* @returns {Promise<User>} The created user object.
* @throws {ValidationError} If the input data is invalid.
*
* @example
* const user = await createUser({
* name: 'Alice',
* email: 'alice@example.com'
* });
*/
async function createUser(userData) {
// Implementation
}
Environment Configuration
1. Use Environment Variables
// config.js
module.exports = {
port: process.env.PORT || 3000,
nodeEnv: process.env.NODE_ENV || 'development',
database: {
url: process.env.DATABASE_URL,
poolSize: parseInt(process.env.DB_POOL_SIZE) || 10
}
};
2. Validate Configuration at Startup
function validateConfig(config) {
const required = ['DATABASE_URL', 'JWT_SECRET'];
const missing = required.filter(key => !process.env[key]);
if (missing.length > 0) {
throw new Error(`Missing required env vars: ${missing.join(', ')}`);
}
}
// Call at startup
validateConfig();
Summary Checklist
Code Quality
- Use consistent naming conventions
- Prefer const over let
- Write meaningful variable names
- Keep functions small and focused
- Use ES6+ features appropriately
Error Handling
- Always handle errors explicitly
- Use custom error classes
- Implement centralized error handling
- Log errors with context
Security
- Validate all user input
- Sanitize data before use
- Use parameterized queries
- Never expose sensitive data
- Keep dependencies updated
Performance
- Use async operations
- Avoid memory leaks
- Use streams for large data
- Monitor and profile regularly
Key Takeaways
- Organize code with a clear, consistent structure
- Follow naming conventions and coding standards
- Always handle errors - never swallow them
- Validate and sanitize all user input
- Use environment variables for configuration
- Write testable, modular code
- Document your code with JSDoc
- Monitor performance and memory usage
- Keep dependencies minimal and updated
Summary
Following best practices helps you build Node.js applications that are maintainable, secure, and performant. You've learned about project structure, coding standards, error handling, security, performance optimization, and testing patterns. These guidelines will serve you well as you build increasingly complex applications.
Congratulations on completing this Node.js basics course! You now have a solid foundation for building server-side JavaScript applications.
Quiz
Question 1 of 617% Complete
0 of 6 questions answered

