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 feature
  • fix - Bug fix
  • docs - Documentation only
  • style - Code style (formatting, semicolons, etc)
  • refactor - Code refactoring
  • perf - Performance improvement
  • test - Adding tests
  • chore - 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