Writing Reusable Utility Functions without Over-Engineering
I once worked on a codebase with a utils/ folder containing 47 files. Most of them had one function. Many of those functions were used exactly once. The team had taken "Don't Repeat Yourself" so literally that they created abstractions for everything.
The irony? Finding and using these utils was harder than just writing the code inline. DRY had become a burden.
Let me share what I've learned about when to create utilities—and when not to.
The Rule of Three
This is my main guideline: don't abstract until you've written similar code three times.
The first time you write something, just write it. The second time, you might notice similarities, but resist the urge. By the third time, you actually understand the pattern and can create a good abstraction.
// First time: just write it
const userName = user.firstName + ' ' + user.lastName;
// Second time: hmm, similar... still just write it
const authorName = author.firstName + ' ' + author.lastName;
// Third time: okay, there's a pattern
const reviewerName = reviewer.firstName + ' ' + reviewer.lastName;
// NOW create the utility
function getFullName(person) {
return `${person.firstName} ${person.lastName}`;
}
Why wait? Because requirements change. Maybe the second use case actually needs middle names. Maybe the third needs "Last, First" format. If you abstract too early, you end up with:
// Over-engineered from premature abstraction
function getFullName(person, options = {}) {
const { includeMiddle, lastFirst, separator = ' ' } = options;
if (lastFirst) {
return includeMiddle
? `${person.lastName}${separator}${person.firstName}${separator}${person.middleName}`
: `${person.lastName}${separator}${person.firstName}`;
}
return includeMiddle
? `${person.firstName}${separator}${person.middleName}${separator}${person.lastName}`
: `${person.firstName}${separator}${person.lastName}`;
}
Now nobody knows how to use it without reading the source code. Three simple inline expressions would have been clearer.
Good Utilities Are Pure
The best utility functions:
- Take input, return output — no side effects
- Don't depend on external state — same input = same output
- Don't know about your business domain — work with primitives
// ✅ Good utility: pure, generic, predictable
function formatCurrency(amount, currency = 'USD') {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency
}).format(amount);
}
function debounce(fn, delay) {
let timeoutId;
return (...args) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn(...args), delay);
};
}
function groupBy(array, key) {
return array.reduce((groups, item) => {
const group = item[key];
groups[group] = groups[group] || [];
groups[group].push(item);
return groups;
}, {});
}
// ❌ Bad utility: knows too much about your app
function formatUserPrice(user, product) {
const discount = user.isPremium ? 0.1 : 0;
const price = product.price * (1 - discount);
return '$' + price.toFixed(2);
}
// This isn't a utility, it's business logic wearing a utility costume
When Copy-Paste Is Fine
Sometimes duplication is the right call:
- The code is trivial. A one-liner doesn't need a utility.
- The contexts are different. Similar code serving different purposes might evolve differently.
- The abstraction would be awkward. If naming it is hard, maybe it shouldn't exist.
// This doesn't need to be a utility
const isAdult = user.age >= 18;
// Neither does this
const initials = name.split(' ').map(n => n[0]).join('');
// Two lines of code used in one place? Just inline it.
const sortedUsers = [...users].sort((a, b) => a.name.localeCompare(b.name));
"But what if I need it again?" Then extract it when you actually need it. The code isn't going anywhere.
Lodash vs Rolling Your Own
Lodash exists. It's battle-tested. If you need debounce, throttle, groupBy, chunk, etc., just use Lodash.
import { debounce, groupBy, chunk } from 'lodash-es';
// Don't reinvent these
When to write your own:
- It's genuinely specific to your domain
- You need something slightly different from what Lodash offers
- Bundle size is critical and you only need one function
When I join a project, a bloated utils/ folder is a red flag. It usually means the team abstracts things too eagerly. A small set of well-used utilities is a sign of maturity.
Organizing Utilities
If you do create utilities, organize them by what they operate on:
utils/
date.js // formatDate, parseDate, daysUntil
string.js // capitalize, truncate, slugify
array.js // groupBy, chunk, unique
currency.js // formatCurrency, parseCurrency
validation.js // isEmail, isPhone, isURL
Not by feature. utils/userUtils.js usually contains business logic that belongs in a service, not a utility.
My Actual Utils File
After years of coding, here's what I actually reuse across projects:
// The classics
export const debounce = (fn, ms) => { /* ... */ };
export const throttle = (fn, ms) => { /* ... */ };
// Formatting
export const formatDate = (date, locale = 'en-US') =>
new Date(date).toLocaleDateString(locale);
export const formatCurrency = (amount, currency = 'USD') =>
new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(amount);
// Validation
export const isEmail = (str) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(str);
// Helpers
export const sleep = (ms) => new Promise(r => setTimeout(r, ms));
export const randomId = () => Math.random().toString(36).slice(2);
// That's basically it. Maybe 10-15 functions total.
Everything else either comes from a library or gets written inline because it's specific to the project.
The Bottom Line
Write code inline until you can't stand the duplication anymore. Then, and only then, extract a utility. Keep it pure, keep it simple, and resist the urge to make it "flexible" with options and flags.
A good utility folder is small. If yours is growing out of control, you're probably abstracting too much.