DocsContributingBest practices

Best practices

Following these best practices helps maintain code quality, consistency, and security across the lomi. project.

Code style

Maintain clean, readable code. We use Prettier and ESLint for automatic formatting and linting (bun run lint:fix).

1. Formatting and Readability

Code Style Example
// Good: Readable, consistent formatting, meaningful names
function isValidAmount(amount: number, maxAmount: number = 1000000): boolean {
  return amount > 0 && amount <= maxAmount;
}
 
// Bad: Poor formatting, short unclear names, magic numbers
# function validate(a){return a>0&&a<=1000000}

Code standards

Maintain high code quality by following these standards:

1. TypeScript

Use TypeScript for type safety and clarity.

TypeScript Example
// Good: Clear interface and types
interface PaymentRequest {
  amount: number; // Amount in smallest currency unit (e.g., cents)
  currency: string; // ISO 4217 currency code (e.g., 'XOF')
  providerCode: string; // Unique identifier for the payment provider
}
 
async function processPayment(request: PaymentRequest): Promise<PaymentResult> {
  // Implementation...
  // Use types for return values as well
}

Security

Security is paramount. Follow these guidelines to protect user data and system integrity.

1. Environment variables

Never commit sensitive keys or secrets directly into the codebase. Use environment variables.

.env.example
# .env.example - Provide a template for required variables
LOMI_API_KEY=
LOMI_WEBHOOK_SECRET=
NODE_ENV=development
.gitignore
# .gitignore - Ensure local environment files are not tracked
.env
.env.local
*.log

Refer to the API Environment documentation for managing API keys.

2. Sensitive data handling

Avoid logging sensitive information like API keys, passwords, or personal user data. If necessary, ensure it’s properly redacted.

Redaction Example
// Example: Redact sensitive information in logs
const sanitizeData = (data: any) => {
  const masked = { ...data };
  // Define sensitive keys specific to your context
  const sensitiveKeys = ['apiKey', 'secretKey', 'password', 'authorization', 'phoneNumber'];
  sensitiveKeys.forEach(key => {
    if (masked[key]) masked[key] = '[REDACTED]';
  });
  return masked;
};

Always validate and sanitize user input to prevent injection attacks. Consider practices like Webhook Signature Verification to ensure data integrity.

Error Handling

Handle errors gracefully and provide context.

1. Graceful Handling

Error Handling Example
// Good: Catch specific errors, provide context, log appropriately
import { logger } from './logger'; // Assuming a logger utility
 
try {
  const result = await processPayment(request);
  logger.info({ transactionId: result.id }, 'Payment successful');
} catch (error) {
  if (error instanceof ValidationError) {
    logger.warn({ error, request }, 'Invalid payment request');
    // Return specific error response to client
  } else if (error instanceof PaymentProviderError) {
    logger.error({ error, request }, 'Payment provider failed');
    // Handle provider-specific failure (e.g., retry logic, alert)
  } else {
    logger.error({ error, request }, 'Unexpected error during payment processing');
    // Handle unknown error
  }
}
 
# Bad: Ignores error, logs vaguely
/*
try {
  await processPayment(request);
} catch (error) {
  console.error("Something went wrong"); // Lacks context
}
*/

Testing

Write meaningful tests using Vitest (describe, it, expect). Aim for comprehensive unit, integration, and end-to-end tests.

Testing Example (Vitest)
import { describe, it, expect } from 'vitest';
import { processPayment } from '../src/payment-processor'; // Adjust path
 
describe('Payment Processing', () => {
  it('should process a valid payment request', async () => {
    const request: PaymentRequest = {
      amount: 10000, // e.g., 100.00 XOF
      currency: 'XOF',
      providerCode: 'PROVIDER_X',
    };
 
    // Mock dependencies if necessary
    // vi.mock(...)
 
    const result = await processPayment(request);
    expect(result).toBeDefined();
    expect(result.status).toBe('completed');
    // Add more specific assertions
  });
 
  it('should throw validation error for invalid amount', async () => {
    const request: PaymentRequest = {
      amount: -500,
      currency: 'XOF',
      providerCode: 'PROVIDER_X',
    };
 
    await expect(processPayment(request)).rejects.toThrow(ValidationError);
  });
});

Git workflow

Adhere to our established Git workflow for smooth collaboration.

1. Branch management

Keep feature branches focused on a single task and relatively short-lived. Regularly rebase with the develop branch.

Terminal
# Create a focused feature branch
git checkout develop
git pull upstream develop
git checkout -b feature/add-wave-provider
 
# Work on the feature...
 
# Commit changes following guidelines
git commit -m "feat(payments): implement Wave provider"
 
# Push the branch
git push origin feature/add-wave-provider

See the full Branching Strategy for details.

2. Commit messages

Use conventional commit messages to provide clarity and enable automated changelog generation.

Terminal - Good Commits
# Good: Specific type, scope, and description
git commit -m "feat(auth): implement API key generation endpoint"
git commit -m "fix(webhooks): handle duplicate event processing"
git commit -m "docs(contributing): clarify rebase workflow"
 
# Bad: Vague, uninformative
# git commit -m "fixed stuff"
# git commit -m "wip"
# git commit -m "more changes"

Documentation

Good documentation is essential for maintainability and collaboration.

1. Code comments

Use comments to explain why something is done, not what it does (the code should explain the what). Use TSDoc for functions and complex logic.

TSDoc Example
/**
 * Processes a payment request using the specified provider.
 * Handles potential errors and ensures idempotency.
 * @param request - The payment request details.
 * @returns A promise resolving to the payment result.
 * @throws {ValidationError} If the request payload is invalid.
 * @throws {PaymentProcessingError} If the provider fails.
 * @example
 * ```typescript
 * const result = await processPayment({ amount: 5000, currency: 'XOF', providerCode: 'WAVE' });
 * console.log(result.transactionId);
 * ```
 */
async function processPayment(
  request: PaymentRequest
): Promise<PaymentResult> {
  // Implementation details...
}

2. README files

Ensure packages and significant components have README files explaining their purpose, usage, configuration, and how to run tests.

README Example
# Package/Component Name
 
## Overview
A brief description of what this package/component does.
 
## Installation
`pnpm add @lomi/package-name`
 
## Usage
Provide clear code examples and instructions.
 
## Configuration
Detail any available configuration options.
 
## Testing
Explain how to run tests for this specific package/component.
`pnpm --filter @lomi/package-name test`

Deployment

Follow standard procedures for deployment.

1. CI/CD

Continuous Integration and Continuous Deployment pipelines automate testing and deployment. Ensure your changes pass all checks.

.github/workflows/ci.yml Example
# Example: .github/workflows/ci.yml (Simplified)
name: CI Checks
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: pnpm/action-setup@v2
        with:
          version: 8 # Specify pnpm version
      - uses: actions/setup-node@v3
        with:
          node-version: '18' # Specify Node.js version
          cache: 'pnpm'
      - run: pnpm install
      - run: pnpm lint
      - run: pnpm test

2. Version control

Package versions are managed within their respective package.json files and updated according to our Versioning guide.

package.json Example
// Example: packages/api/package.json
{
  "name": "@lomi/api",
  "version": "1.2.3",
  "engines": {
    "node": ">=18"
  },
  "main": "dist/index.js"
}

Next steps