Utils & Helpers
The toolbox of reusable functions that make your code cleaner and more consistent.
9 min read
The Toolbox Analogy#
Every craftsperson has a toolbox. Instead of making a new hammer every time you need one, you grab the one from your toolbox.
Utils (utilities) are your code toolbox - small, reusable functions that solve common problems:
- Format a date
- Generate a random ID
- Validate an email
- Hash a password
Write once, use everywhere.
Why Utils Matter#
Without utils:
javascript
// Same code repeated everywhere
const id = Math.random().toString(36).substr(2, 9);
// ... in another file
const anotherId = Math.random().toString(36).substr(2, 9);
With utils:
javascript
import { generateId } from '../utils/id.js';
const id = generateId();
const anotherId = generateId();
Benefits:
- DRY - Don't repeat yourself
- Testable - Test the util once, trust it everywhere
- Consistent - Same behavior across your app
- Easy to improve - Fix in one place, fixed everywhere
Essential Utils#
Error Classes#
javascript
// src/utils/errors.js
export class AppError extends Error {
constructor(message, statusCode, code) {
super(message);
this.statusCode = statusCode;
this.code = code;
this.isOperational = true;
Error.captureStackTrace(this, this.constructor);
}
}
export class NotFoundError extends AppError {
constructor(resource = 'Resource') {
super(`${resource} not found`, 404, 'NOT_FOUND');
}
}
export class BadRequestError extends AppError {
constructor(message = 'Bad request') {
super(message, 400, 'BAD_REQUEST');
}
}
export class UnauthorizedError extends AppError {
constructor(message = 'Unauthorized') {
super(message, 401, 'UNAUTHORIZED');
}
}
export class ForbiddenError extends AppError {
constructor(message = 'Forbidden') {
super(message, 403, 'FORBIDDEN');
}
}
export class ConflictError extends AppError {
constructor(message = 'Resource already exists') {
super(message, 409, 'CONFLICT');
}
}
export class ValidationError extends AppError {
constructor(message = 'Validation failed', details = []) {
super(message, 400, 'VALIDATION_ERROR');
this.details = details;
}
}
Response Helpers#
javascript
// src/utils/response.js
export function success(res, data, statusCode = 200) {
res.status(statusCode).json({ data });
}
export function created(res, data) {
res.status(201).json({ data });
}
export function noContent(res) {
res.status(204).send();
}
export function paginated(res, data, pagination) {
res.json({
data,
pagination: {
page: pagination.page,
limit: pagination.limit,
total: pagination.total,
pages: Math.ceil(pagination.total / pagination.limit),
hasNext: pagination.page < Math.ceil(pagination.total / pagination.limit),
hasPrev: pagination.page > 1,
},
});
}
Crypto Utils#
javascript
// src/utils/crypto.js
import bcrypt from 'bcrypt';
import crypto from 'crypto';
import jwt from 'jsonwebtoken';
import { config } from '../config/index.js';
const SALT_ROUNDS = 10;
export async function hashPassword(password) {
return bcrypt.hash(password, SALT_ROUNDS);
}
export async function comparePassword(password, hash) {
return bcrypt.compare(password, hash);
}
export function generateToken(payload, expiresIn = config.jwt.expiresIn) {
return jwt.sign(payload, config.jwt.secret, { expiresIn });
}
export function verifyToken(token) {
return jwt.verify(token, config.jwt.secret);
}
export function generateRandomToken(length = 32) {
return crypto.randomBytes(length).toString('hex');
}
export function generateOTP(length = 6) {
const digits = '0123456789';
let otp = '';
for (let i = 0; i < length; i++) {
otp += digits[Math.floor(Math.random() * 10)];
}
return otp;
}
export function hashToken(token) {
return crypto.createHash('sha256').update(token).digest('hex');
}
Cache Utils#
javascript
// src/utils/cache.js
import { redis } from '../config/redis.js';
export const cache = {
async get(key) {
try {
const data = await redis.get(key);
return data ? JSON.parse(data) : null;
} catch (error) {
console.error('Cache get error:', error.message);
return null;
}
},
async set(key, value, ttl = 300) {
try {
await redis.set(key, JSON.stringify(value), { EX: ttl });
return true;
} catch (error) {
console.error('Cache set error:', error.message);
return false;
}
},
async del(key) {
try {
await redis.del(key);
return true;
} catch (error) {
console.error('Cache del error:', error.message);
return false;
}
},
async exists(key) {
try {
return (await redis.exists(key)) === 1;
} catch (error) {
return false;
}
},
// Delete keys matching a pattern
async delPattern(pattern) {
try {
const keys = await redis.keys(pattern);
if (keys.length > 0) {
await redis.del(keys);
}
return true;
} catch (error) {
console.error('Cache delPattern error:', error.message);
return false;
}
},
};
Date Utils#
javascript
// src/utils/date.js
export function formatDate(date, format = 'YYYY-MM-DD') {
const d = new Date(date);
const tokens = {
YYYY: d.getFullYear(),
MM: String(d.getMonth() + 1).padStart(2, '0'),
DD: String(d.getDate()).padStart(2, '0'),
HH: String(d.getHours()).padStart(2, '0'),
mm: String(d.getMinutes()).padStart(2, '0'),
ss: String(d.getSeconds()).padStart(2, '0'),
};
return format.replace(/YYYY|MM|DD|HH|mm|ss/g, match => tokens[match]);
}
export function addDays(date, days) {
const result = new Date(date);
result.setDate(result.getDate() + days);
return result;
}
export function addHours(date, hours) {
return new Date(date.getTime() + hours * 60 * 60 * 1000);
}
export function isExpired(date) {
return new Date(date) < new Date();
}
export function getStartOfDay(date = new Date()) {
const d = new Date(date);
d.setHours(0, 0, 0, 0);
return d;
}
export function getEndOfDay(date = new Date()) {
const d = new Date(date);
d.setHours(23, 59, 59, 999);
return d;
}
export function daysBetween(date1, date2) {
const oneDay = 24 * 60 * 60 * 1000;
return Math.round(Math.abs((date1 - date2) / oneDay));
}
String Utils#
javascript
// src/utils/string.js
export function slugify(text) {
return text
.toLowerCase()
.trim()
.replace(/[^\w\s-]/g, '')
.replace(/[\s_-]+/g, '-')
.replace(/^-+|-+$/g, '');
}
export function truncate(text, length = 100, suffix = '...') {
if (text.length <= length) return text;
return text.slice(0, length - suffix.length).trim() + suffix;
}
export function capitalize(text) {
return text.charAt(0).toUpperCase() + text.slice(1).toLowerCase();
}
export function titleCase(text) {
return text
.toLowerCase()
.split(' ')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
}
export function generateUsername(email) {
return email.split('@')[0].replace(/[^a-zA-Z0-9]/g, '').toLowerCase();
}
export function maskEmail(email) {
const [name, domain] = email.split('@');
const masked = name.slice(0, 2) + '***' + name.slice(-1);
return `${masked}@${domain}`;
}
export function parseFullName(fullName) {
const parts = fullName.trim().split(/\s+/);
return {
firstName: parts[0] || '',
lastName: parts.slice(1).join(' ') || '',
};
}
ID Generation#
javascript
// src/utils/id.js
import crypto from 'crypto';
export function generateId(length = 12) {
return crypto.randomBytes(length).toString('hex').slice(0, length);
}
export function generateUUID() {
return crypto.randomUUID();
}
export function generateShortId() {
return Date.now().toString(36) + Math.random().toString(36).substr(2, 5);
}
// URL-safe base64 ID
export function generateUrlSafeId(length = 16) {
return crypto.randomBytes(length)
.toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}
Pagination Helper#
javascript
// src/utils/pagination.js
export function getPaginationParams(query) {
const page = Math.max(1, parseInt(query.page, 10) || 1);
const limit = Math.min(100, Math.max(1, parseInt(query.limit, 10) || 20));
const skip = (page - 1) * limit;
return { page, limit, skip };
}
export function paginationMeta(total, page, limit) {
const pages = Math.ceil(total / limit);
return {
total,
page,
limit,
pages,
hasNext: page < pages,
hasPrev: page > 1,
};
}
Query Builder Helpers#
javascript
// src/utils/query.js
export function buildSortObject(sortString) {
if (!sortString) return { createdAt: -1 };
const sort = {};
sortString.split(',').forEach(field => {
if (field.startsWith('-')) {
sort[field.slice(1)] = -1;
} else {
sort[field] = 1;
}
});
return sort;
}
// ?sort=-createdAt,name → { createdAt: -1, name: 1 }
export function buildFilterObject(filters, allowedFields) {
const query = {};
for (const [key, value] of Object.entries(filters)) {
if (!allowedFields.includes(key) || value === undefined) continue;
// Handle special operators
if (typeof value === 'object') {
if (value.gte) query[key] = { ...query[key], $gte: value.gte };
if (value.lte) query[key] = { ...query[key], $lte: value.lte };
if (value.in) query[key] = { $in: value.in.split(',') };
} else {
query[key] = value;
}
}
return query;
}
export function buildSearchQuery(searchTerm, fields) {
if (!searchTerm) return {};
return {
$or: fields.map(field => ({
[field]: { $regex: searchTerm, $options: 'i' },
})),
};
}
Async Utilities#
javascript
// src/utils/async.js
export function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
export async function retry(fn, options = {}) {
const { retries = 3, delay = 1000, backoff = 2 } = options;
for (let attempt = 1; attempt <= retries; attempt++) {
try {
return await fn();
} catch (error) {
if (attempt === retries) throw error;
await sleep(delay * Math.pow(backoff, attempt - 1));
}
}
}
// Usage
const result = await retry(
() => fetchExternalAPI(),
{ retries: 3, delay: 1000 }
);
export function timeout(promise, ms) {
return Promise.race([
promise,
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), ms)
),
]);
}
// Usage
const result = await timeout(slowOperation(), 5000);
Organizing Utils#
Single File for Small Projects#
src/utils/
├── index.js # Export everything
├── errors.js
├── response.js
├── crypto.js
├── cache.js
└── helpers.js # Misc small functions
By Domain for Larger Projects#
src/utils/
├── index.js
├── errors/
│ ├── index.js
│ └── types.js
├── http/
│ ├── response.js
│ └── request.js
├── crypto/
│ ├── hash.js
│ └── tokens.js
└── db/
├── pagination.js
└── query.js
Index File#
javascript
// src/utils/index.js
export * from './errors.js';
export * from './response.js';
export * from './crypto.js';
export * from './cache.js';
export * from './date.js';
export * from './string.js';
export * from './pagination.js';
// Now import from one place
import { NotFoundError, hashPassword, slugify } from '../utils/index.js';
Testing Utils#
Utils are easy to test - they're pure functions:
javascript
// tests/utils/string.test.js
import { slugify, truncate, maskEmail } from '../../src/utils/string.js';
describe('string utils', () => {
describe('slugify', () => {
it('converts to lowercase', () => {
expect(slugify('Hello World')).toBe('hello-world');
});
it('removes special characters', () => {
expect(slugify('Hello! World?')).toBe('hello-world');
});
it('handles multiple spaces', () => {
expect(slugify('Hello World')).toBe('hello-world');
});
});
describe('truncate', () => {
it('truncates long text', () => {
expect(truncate('Hello World', 8)).toBe('Hello...');
});
it('keeps short text unchanged', () => {
expect(truncate('Hi', 10)).toBe('Hi');
});
});
describe('maskEmail', () => {
it('masks email correctly', () => {
expect(maskEmail('john.doe@example.com')).toBe('jo***e@example.com');
});
});
});
Key Takeaways#
- Utils are small, reusable functions - One job, done well
- Keep them pure when possible - Same input, same output, no side effects
- Group logically - errors, crypto, strings, dates
- Export from index - Clean imports
- Test thoroughly - Utils are easy to test, so test them well
The Rule
If you're writing the same code in two places, it belongs in utils. If the function is more than 20 lines, it might belong in a service instead.
Continue Learning
Ready to level up your skills?
Explore more guides and tutorials to deepen your understanding and become a better developer.