learn.colinkim.dev

Reusable utility modules

Learn how to write utility functions that are general enough to reuse across projects, focused enough to be reliable, and clean enough to read at a glance.

Utility modules contain functions that solve a common problem and can be imported wherever needed. They are the building blocks that keep the rest of the codebase free of duplication.

What makes a good utility

A good utility function:

  • does one thing and does it well
  • does not depend on global state or side effects
  • accepts all the data it needs as arguments
  • returns a result instead of mutating inputs
  • has a name that describes what it does
// Good utility — focused, no side effects, returns a new value
function clamp(value, min, max) {
  return Math.max(min, Math.min(max, value));
}
// Not a good utility — depends on global state
function clamp(value) {
  return Math.max(globalMin, Math.min(globalMax, value));
}

The first version is reusable anywhere. The second version ties itself to variables that must exist in the global scope.

Organizing a utility module

Keep related utilities in the same module. Group by purpose, not by size:

// format.js
export function formatDate(date) {
  return date.toISOString().split("T")[0];
}

export function formatCurrency(amount, currency = "USD") {
  return new Intl.NumberFormat("en-US", { style: "currency", currency }).format(amount);
}

export function truncate(text, maxLength) {
  return text.length > maxLength ? text.slice(0, maxLength) + "..." : text;
}
// validate.js
export function isEmail(value) {
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
}

export function isRequired(value) {
  return value !== null && value !== undefined && value !== "";
}

export function minLength(value, min) {
  return value.length >= min;
}

Each module covers one concern: formatting or validation. Consumers import only what they need.

Avoiding over-utility

Not every function needs to be in a utility module. Some functions are specific to one feature and should live there:

// Bad — too specific for a utility
export function getUserDisplayName(user) {
  return `${user.firstName} ${user.lastName}`;
}

This belongs in a users.js module, not a general format.js utility. The rule: if a function would only ever be used in one place, it does not need to be a utility.

Pure functions make the best utilities

A pure function always returns the same output for the same input and has no side effects:

// Pure — same input always gives same output, no side effects
function slugify(text) {
  return text.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "");
}

// Impure — depends on Date.now(), different result each call
function getTimestamp() {
  return Date.now();
}

Pure functions are predictable, testable, and composable. They make the best utilities. But not every utility can be pure — logging, random numbers, and time access are inherently impure. That is fine as long as the impurity is clear.

Exporting utilities conditionally

Sometimes a utility needs a default value or configuration:

// api.js
export function createApiClient(baseUrl) {
  return {
    get(path) {
      return fetch(`${baseUrl}${path}`).then((r) => r.json());
    },
    post(path, data) {
      return fetch(`${baseUrl}${path}`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(data),
      }).then((r) => r.json());
    },
  };
}

This is a factory function that returns a configured object. It is more flexible than hardcoding baseUrl as a constant.

What to carry forward

  • utilities should do one thing, accept all needed data as arguments, and return results
  • avoid dependencies on global state — pass everything the function needs as a parameter
  • group utilities by purpose in modules like format.js or validate.js
  • pure functions make the best utilities — predictable, testable, composable
  • not every function needs to be a utility — feature-specific functions belong in feature modules
  • utilities benefit from testing because they are used across the codebase

Utilities are the glue between modules. The next unit covers asynchronous JavaScript — the paradigm for handling operations that take time.

Progress

Quick checks

No quick checks in this lesson.

Mark lesson manually or answer quick checks to track progress.