Coding Standards
Style guide and best practices for the Notification Service codebase.
TypeScript Guidelines
Type Safety
Always use explicit types, avoid any.
Bad:
function processData(data: any): any {
return data.value;
}
Good:
interface InputData {
value: string;
}
function processData(data: InputData): string {
return data.value;
}
Type Assertions
Use type assertions sparingly, prefer type guards.
Bad:
const value = data as string;
Good:
if (typeof data === 'string') {
const value = data;
}
Null Safety
Use optional chaining and nullish coalescing.
Bad:
const email = user && user.email ? user.email : 'default@example.com';
Good:
const email = user?.email ?? 'default@example.com';
Enums vs Constants
Use const objects over enums for better tree-shaking.
Bad:
enum Status {
Queued = 1,
Sent = 2
}
Good:
export const NOTIFICATION_STATUSES = {
QUEUED: 1,
SENT: 2
} as const;
Code Organization
File Structure
Each file should have a single responsibility.
Typical file structure:
// 1. Imports
import express from 'express';
import logger from '../config/logger';
// 2. Types/Interfaces
interface NotificationData {
email: string;
subject: string;
}
// 3. Constants
const MAX_RETRIES = 3;
// 4. Functions/Classes
class NotificationService {
async send(data: NotificationData): Promise<void> {
// Implementation
}
}
// 5. Exports
export default new NotificationService();
Function Size
Keep functions small and focused (max 50 lines).
Bad:
async function processNotification(data) {
// 100+ lines of code
// Multiple responsibilities
// Hard to test
}
Good:
async function processNotification(data: NotificationData): Promise<void> {
await validateData(data);
const notification = await createNotification(data);
await queueForDelivery(notification);
}
async function validateData(data: NotificationData): Promise<void> {
// Validation logic
}
async function createNotification(data: NotificationData): Promise<Notification> {
// Creation logic
}
async function queueForDelivery(notification: Notification): Promise<void> {
// Queue logic
}
Class Design
Prefer composition over inheritance.
Bad:
class EmailNotification extends Notification {
// Deep inheritance hierarchy
}
Good:
class EmailNotification {
constructor(
private emailService: EmailService,
private repository: NotificationRepository
) {}
}
Error Handling
Custom Errors
Use custom error classes for different error types.
class ValidationError extends Error {
statusCode = 400;
code = 'VALIDATION_ERROR';
constructor(message: string, public details: any[]) {
super(message);
this.name = 'ValidationError';
}
}
// Usage
throw new ValidationError('Invalid email', [
{ field: 'email', message: 'Invalid format' }
]);
Error Propagation
Let errors bubble up to the error handler middleware.
Bad:
async function sendEmail(data: EmailData) {
try {
await smtp.send(data);
} catch (error) {
console.error('Error:', error);
return { success: false, error: 'Failed to send' };
}
}
Good:
async function sendEmail(data: EmailData): Promise<void> {
try {
await smtp.send(data);
} catch (error) {
logger.error('SMTP error', { error: error.message, data });
throw new ServiceUnavailableError('SMTP server unavailable');
}
}
Try-Catch Placement
Use try-catch at boundaries (routes, workers).
// Route handler
router.post('/send', async (req, res, next) => {
try {
const result = await notificationService.send(req.body);
res.json({ success: true, data: result });
} catch (error) {
next(error); // Pass to error handler
}
});
Async/Await
Always Use Async/Await
Prefer async/await over raw promises.
Bad:
function getNotification(id: string) {
return db.query('SELECT * FROM notifications WHERE id = $1', [id])
.then(result => result.rows[0])
.catch(error => {
logger.error('Query failed', { error });
throw error;
});
}
Good:
async function getNotification(id: string): Promise<Notification> {
try {
const result = await db.query('SELECT * FROM notifications WHERE id = $1', [id]);
return result.rows[0];
} catch (error) {
logger.error('Query failed', { error: error.message, id });
throw error;
}
}
Parallel Operations
Use Promise.all() for independent operations.
Bad (Sequential):
const user = await getUser(userId);
const stats = await getStats(userId);
const notifications = await getNotifications(userId);
Good (Parallel):
const [user, stats, notifications] = await Promise.all([
getUser(userId),
getStats(userId),
getNotifications(userId)
]);
Logging
Structured Logging
Always use structured logs with context.
Bad:
console.log('Email sent to user@example.com');
Good:
logger.info('Email sent', {
notificationId: notification.id,
email: notification.channel,
userId: notification.userId,
duration: Date.now() - startTime
});
Log Levels
Use appropriate log levels.
logger.error('Database connection failed', { error: err.message }); // Errors
logger.warn('Queue depth high', { depth: 50000 }); // Warnings
logger.info('Server started', { port: 3001 }); // Info
logger.debug('Request received', { method: 'POST', path: '/send' }); // Debug
Sensitive Data
Never log sensitive data.
Bad:
logger.info('User authenticated', {
email: user.email,
password: user.password // Never log passwords!
});
Good:
logger.info('User authenticated', {
userId: user.id,
email: user.email
});
Database Queries
Parameterized Queries
Always use parameterized queries (prevent SQL injection).
Bad:
const query = `SELECT * FROM notifications WHERE id = '${id}'`;
const result = await db.query(query);
Good:
const query = 'SELECT * FROM notifications WHERE id = $1';
const result = await db.query(query, [id]);
Transaction Management
Use transactions for multi-step operations.
async function transferNotifications(fromUser: string, toUser: string): Promise<void> {
const client = await db.getClient();
try {
await client.query('BEGIN');
await client.query('UPDATE notifications SET "userId" = $1 WHERE "userId" = $2', [toUser, fromUser]);
await client.query('INSERT INTO audit_log (action, userId) VALUES ($1, $2)', ['transfer', fromUser]);
await client.query('COMMIT');
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
client.release();
}
}
Query Optimization
Use indexes and limit result sets.
// Add LIMIT for large datasets
async function getNotifications(userId: string, limit = 50): Promise<Notification[]> {
const result = await db.query(
'SELECT * FROM notifications WHERE "userId" = $1 ORDER BY "createdAt" DESC LIMIT $2',
[userId, limit]
);
return result.rows;
}
Testing
Test Structure
Follow Arrange-Act-Assert pattern.
describe('NotificationRepository', () => {
describe('create', () => {
it('should create notification with valid data', async () => {
// Arrange
const data = {
userId: '123',
type: 'email',
channel: 'user@example.com',
subject: 'Test',
content: 'Hello'
};
// Act
const notification = await repository.create(data);
// Assert
expect(notification.id).toBeDefined();
expect(notification.userId).toBe(data.userId);
expect(notification.statusId).toBe(NOTIFICATION_STATUSES.QUEUED);
});
});
});
Test Naming
Use descriptive test names.
Bad:
it('works', () => { ... });
Good:
it('should return 404 when notification not found', () => { ... });
Mocking
Mock external dependencies.
import { vi } from 'vitest';
describe('EmailWorker', () => {
it('should send email via SMTP', async () => {
// Mock SMTP service
const mockSend = vi.fn().mockResolvedValue(undefined);
const emailService = { send: mockSend };
const worker = new EmailWorker(emailService);
await worker.processMessage(mockMessage);
expect(mockSend).toHaveBeenCalledWith({
to: 'user@example.com',
subject: 'Test',
html: expect.any(String)
});
});
});
Comments
When to Comment
Comment "why", not "what".
Bad:
// Increment retry count
retryCount++;
Good:
// Retry count is used to determine exponential backoff delay
retryCount++;
const delay = calculateBackoff(retryCount);
JSDoc
Use JSDoc for public APIs.
/**
* Sends verification email to user
* @param data - Verification email data
* @param data.email - Recipient email address
* @param data.username - User's display name
* @param data.verificationLink - HTTPS URL for verification
* @returns Promise resolving to notification ID
* @throws {ValidationError} If email is invalid
* @throws {ServiceUnavailableError} If queue is unavailable
*/
async function sendVerificationEmail(data: VerificationEmailData): Promise<string> {
// Implementation
}
TODO Comments
Use TODO for future improvements.
// TODO: Add support for HTML templates
// TODO: Implement exponential backoff with jitter
// TODO: Add circuit breaker for SMTP failures
Performance
Avoid N+1 Queries
Batch database operations.
Bad:
for (const notification of notifications) {
await updateStatus(notification.id, 'sent');
}
Good:
const ids = notifications.map(n => n.id);
await db.query(
'UPDATE notifications SET "statusId" = $1 WHERE id = ANY($2)',
[NOTIFICATION_STATUSES.SENT, ids]
);
Connection Pooling
Reuse database connections.
// Use pool, not individual connections
const result = await pool.query('SELECT * FROM notifications');
// Not:
const client = await pool.connect();
const result = await client.query('SELECT * FROM notifications');
client.release();
Caching
Cache expensive operations.
class NotificationRepository {
private statusCache = new Map<number, string>();
async getStatusName(statusId: number): Promise<string> {
if (this.statusCache.has(statusId)) {
return this.statusCache.get(statusId)!;
}
const result = await db.query('SELECT name FROM notification_statuses WHERE id = $1', [statusId]);
const name = result.rows[0].name;
this.statusCache.set(statusId, name);
return name;
}
}
Security
Input Validation
Validate all user input.
import Joi from 'joi';
const schema = Joi.object({
email: Joi.string().email().required(),
subject: Joi.string().max(500).required(),
message: Joi.string().required()
});
const { error, value } = schema.validate(req.body);
if (error) {
throw new ValidationError('Invalid input', error.details);
}
SQL Injection Prevention
Always use parameterized queries.
// Safe
await db.query('SELECT * FROM notifications WHERE id = $1', [id]);
// NEVER DO THIS:
await db.query(`SELECT * FROM notifications WHERE id = '${id}'`);
Secrets Management
Never hardcode secrets.
Bad:
const apiKey = 'sk_live_1234567890abcdef';
Good:
const apiKey = process.env.API_KEY;
if (!apiKey) {
throw new Error('API_KEY environment variable is required');
}
Code Review Checklist
Before Submitting PR
- All tests pass
- Code is formatted (run
npm run format) - No linter errors (run
npm run lint) - Added tests for new functionality
- Updated documentation if needed
- No sensitive data in code or logs
- Commit messages follow Conventional Commits
- No TODOs without tracking issue
During Review
- Code follows style guide
- Functions are small and focused
- Error handling is comprehensive
- Logging is appropriate
- No performance issues
- Security best practices followed
- Tests are meaningful
Git Workflow
Branch Naming
feat/add-sms-support
fix/email-template-rendering
docs/update-api-examples
refactor/queue-management
test/add-integration-tests
chore/update-dependencies
Commit Messages
Follow Conventional Commits:
<type>: <subject>
<body>
<footer>
Types:
feat- New featurefix- Bug fixdocs- Documentation onlystyle- Code style (formatting, semicolons, etc)refactor- Code refactoringperf- Performance improvementtest- Adding testschore- Maintenance
Example:
feat: add SMS notification support
- Add SMS service with Twilio integration
- Add SMS queue and worker
- Add SMS templates
- Update API to accept phone numbers
Closes #42
Pull Requests
Title: Same as commit message (if single commit)
Description:
## Summary
Brief description of changes
## Changes
- Added feature X
- Fixed bug Y
- Updated documentation Z
## Testing
- [ ] Unit tests added
- [ ] Integration tests added
- [ ] Manual testing completed
## Screenshots (if applicable)
## Checklist
- [ ] Tests pass
- [ ] Documentation updated
- [ ] No breaking changes (or documented)
Next Steps
- Project Structure - Codebase organization
- API Design - REST API structure
- Deployment Guide - Production deployment