DEV Community

Software Solutions
Software Solutions

Posted on

Building Reusable JavaScript Utilities That Scale

Every growing codebase eventually suffers from utility clutter. Functions like formatCurrency(), debounce(), or truncateText() get copied and pasted across multiple UI components, helper files, and backend services.

Over time, minor variations emerge—one version handles null values slightly differently, while another introduces a subtle edge-case bug.

Building clean, reusable, and maintainable utility modules requires deliberate software design: keeping functions pure, enforcing strict type safety, ensuring zero side effects, and supporting modern bundler tree-shaking.

Here is a practical guide to architecting production-grade JavaScript and TypeScript utilities.

1. Principle 1: Pure Functions & Immutability

A great utility function should act like a mathematical formula: given the same inputs, it must always return the exact same output, with zero side effects on global application state or mutation of input arguments.

// ❌ BAD: Mutates the original array passed into the function
export function getTopScorers(users) {
  return users.sort((a, b) => b.score - a.score).slice(0, 3);
}

// ✅ GOOD: Immutably operates on data (using structuredClone or slice)
export function getTopScorers(users) {
  return [...users].sort((a, b) => b.score - a.score).slice(0, 3);
}
Enter fullscreen mode Exit fullscreen mode

2. Principle 2: Enforce Strict Type Safety & Edge Case Handling

Utilities are built to be consumed across dozens of modules. If a utility function fails silently or throws uncaught runtime errors when passed undefined or null, it degrades the reliability of your entire application.

Using TypeScript with explicit generics and nullish coalescing ensures compile-time confidence:

/**
 * Safely truncates a string to a max length without breaking words.
 */
export function truncateText(text: string | null | undefined, maxLength: number = 100): string {
  if (!text) return '';
  if (text.length <= maxLength) return text;

  const truncated = text.slice(0, maxLength);
  const lastSpaceIndex = truncated.lastIndexOf(' ');

  return lastSpaceIndex > 0 
    ? `${truncated.slice(0, lastSpaceIndex)}...` 
    : `${truncated}...`;
}
Enter fullscreen mode Exit fullscreen mode

3. High-Value Utilities Every Team Needs

A. Debounce Implementation with Cancellation

Prevent API request spam or heavy UI recalculations during rapid user events (e.g., search inputs or window resizing):

export function debounce<T (...args: extends> void>(
  fn: T,
  delay: number
): (...args: Parameters<T>) => void {
  let timerId: ReturnType<typeof setTimeout> | null = null;

  return function (...args: Parameters<T>) {
    if (timerId) clearTimeout(timerId);
    timerId = setTimeout(() => {
      fn(...args);
    }, delay);
  };
}
Enter fullscreen mode Exit fullscreen mode

B. Safe Currency Formatting with Intl
Avoid heavy external date/number formatting dependencies by leveraging browser-native internationalization APIs:

export function formatCurrency(
  amount: number | null | undefined,
  currency: string = 'USD',
  locale: string = 'en-US'
): string {
  if (typeof amount !== 'number' || isNaN(amount)) {
    return new Intl.NumberFormat(locale, { style: 'currency', currency }).format(0);
  }

  return new Intl.NumberFormat(locale, {
    style: 'currency',
    currency,
    minimumFractionDigits: 2
  }).format(amount);
}
Enter fullscreen mode Exit fullscreen mode

4. Packaging for Tree-Shaking (ES Modules)

To prevent your utility library from bloating client-side bundle sizes, ensure your codebase is published using ES Module (ESM) syntax. Modern bundlers (Vite, Webpack, Rollup) inspect ESM exports to automatically strip out unused utilities during production builds.

Directory Structure

Organize utilities into modular, single-responsibility files rather than giant monolithic utils.js files:

src/
└── utils/
    ├── async/
    │   ├── debounce.ts
    │   └── throttle.ts
    ├── string/
    │   ├── capitalize.ts
    │   └── truncateText.ts
    ├── number/
    │   └── formatCurrency.ts
    └── index.ts  <-- Named barrel export
Enter fullscreen mode Exit fullscreen mode

Barrel Export Strategy (index.ts):

// Explicitly re-export utilities as named exports
export { debounce } from './async/debounce';
export { throttle } from './async/throttle';
export { truncateText } from './string/truncateText';
export { formatCurrency } from './number/formatCurrency';

Enter fullscreen mode Exit fullscreen mode

5. Testing Reusable Utilities

Because utility functions are heavily reused across an enterprise codebase, writing comprehensive unit tests (using Vitest or Jest) is mandatory.

import { describe, it, expect } from 'vitest';
import { truncateText } from './truncateText';

describe('truncateText()', () => {
  it('returns empty string when input is null or undefined', () => {
    expect(truncateText(null)).toBe('');
    expect(truncateText(undefined)).toBe('');
  });

  it('does not truncate text shorter than maxLength', () => {
    expect(truncateText('Hello World', 20)).toBe('Hello World');
  });

  it('truncates gracefully at word boundaries', () => {
    expect(truncateText('The quick brown fox jumps over', 15)).toBe('The quick...');
  });
});
Enter fullscreen mode Exit fullscreen mode

Developer Takeaways

  1. Keep Functions Pure: Avoid side effects, mutation of input parameters, or hidden global state dependencies.
  2. Defend Against Bad Inputs: Always validate edge cases (null, undefined, empty strings, NaN) early in the function body.
  3. Optimize for Tree-Shaking: Use modular ES import/export syntax so bundlers drop unused code automatically.
  4. Leverage Native Web APIs: Use built-in APIs like Intl, structuredClone, and URLSearchParams before adding heavy external npm dependencies.

Need Scalable Software Engineering Solutions for Your Business?

Building high-performance web applications requires modular code architecture, robust frontend/backend engineering, and modern web standards.

Partner with Software Solutions for custom web application development, frontend architecture design, API development, and enterprise cloud solutions.

Top comments (0)