Every developer has that moment: you need a quick JSON formatter, a regex tester, or a base64 decoder — and you end up on some sketchy third-party site that logs your data. Here are 5 tools you can build yourself in plain vanilla JS, each under 100 lines.
All five are live at profiterole-blog/tools if you want to see them in action first.
1. JSON Formatter
What it does: Takes ugly, minified JSON and pretty-prints it with indentation.
Why it's useful: Debugging API responses is painful when everything is on one line. A local formatter means no copy-pasting sensitive payloads to external sites.
function formatJSON() {
const input = document.getElementById('input').value;
const output = document.getElementById('output');
try {
const parsed = JSON.parse(input);
output.textContent = JSON.stringify(parsed, null, 2);
output.style.color = '';
} catch (e) {
output.textContent = 'Invalid JSON: ' + e.message;
output.style.color = 'red';
}
}
// HTML: <textarea id="input"></textarea>
// <button onclick="formatJSON()">Format</button>
// <pre id="output"></pre>
Bonus: wire the input event for live formatting as you type.
2. Regex Tester
What it does: Tests a regex pattern against input text and shows all matches with capture groups.
Why it's useful: Regex is notoriously hard to get right. A live tester saves hours of trial and error.
function testRegex() {
const pattern = document.getElementById('pattern').value;
const flags = document.getElementById('flags').value || 'g';
const text = document.getElementById('text').value;
const results = document.getElementById('results');
try {
const regex = new RegExp(pattern, flags);
const matches = [...text.matchAll(regex)];
if (!matches.length) { results.textContent = 'No matches.'; return; }
results.textContent = matches.map((m, i) => {
const groups = m.slice(1).map((g, j) =>
' Group '+(j+1)+': '+(g ?? 'undefined')
).join('\n');
return 'Match '+(i+1)+': "'+m[0]+'" @ index '+m.index+'\n'+groups;
}).join('\n\n');
} catch (e) { results.textContent = 'Bad regex: ' + e.message; }
}
matchAll is cleaner than exec in a loop — it handles the g flag without manual index bumping.
3. Base64 Encoder / Decoder
What it does: Encodes plain text or decodes base64 with full UTF-8 support.
Why it's useful: JWTs, API tokens, and email attachments all use base64. Native atob/btoa silently break on non-ASCII — this version handles full Unicode.
function encode() {
const bytes = new TextEncoder().encode(document.getElementById('input').value);
const binary = String.fromCharCode(...bytes);
document.getElementById('output').value = btoa(binary);
}
function decode() {
try {
const binary = atob(document.getElementById('input').value.trim());
const bytes = Uint8Array.from(binary, c => c.charCodeAt(0));
document.getElementById('output').value = new TextDecoder().decode(bytes);
} catch (e) {
document.getElementById('output').value = 'Invalid base64: ' + e.message;
}
}
TextEncoder + btoa is the correct pattern. btoa alone throws on emoji or accented characters.
4. Cron Expression Parser
What it does: Takes a cron string like */5 * * * * and produces a human-readable description.
Why it's useful: Cron syntax is terse. Instantly confirming "every 5 minutes, not every 5 hours" prevents production incidents.
function parseCron(expr) {
const parts = expr.trim().split(/\s+/);
if (parts.length !== 5) return { error: 'Expected 5 fields' };
const [min, hour, dom, month, dow] = parts;
const describe = (v, unit) => v === '*' ? 'every ' + unit : unit + ' ' + v;
return {
summary: [
describe(min, 'minute'), describe(hour, 'hour'),
describe(dom, 'day-of-month'), describe(month, 'month'),
describe(dow, 'weekday'),
].join(', '),
};
}
// parseCron('*/15 9-17 * * 1-5').summary
// => 'minute */15, hour 9-17, every day-of-month, every month, weekday 1-5'
For production use cronstrue. For an internal tool this 20-liner covers ~80% of cases.
5. Color Converter
What it does: Converts between HEX, RGB, and HSL with a live preview swatch.
Why it's useful: CSS work constantly requires format switching. No more context-switching to a browser tab.
function hexToRgb(hex) {
const n = parseInt(hex.replace(/^#/, '').padStart(6,'0'), 16);
return { r: (n>>16)&255, g: (n>>8)&255, b: n&255 };
}
function rgbToHsl(r, g, b) {
r/=255; g/=255; b/=255;
const max=Math.max(r,g,b), min=Math.min(r,g,b);
let h, s, l=(max+min)/2;
if (max===min) { h=s=0; } else {
const d=max-min;
s = l>.5 ? d/(2-max-min) : d/(max+min);
if (max===r) h=((g-b)/d+(g<b?6:0))/6;
else if (max===g) h=((b-r)/d+2)/6;
else h=((r-g)/d+4)/6;
}
return { h:Math.round(h*360), s:Math.round(s*100), l:Math.round(l*100) };
}
function convert() {
const hex = document.getElementById('hex').value;
const {r,g,b} = hexToRgb(hex);
const {h,s,l} = rgbToHsl(r,g,b);
document.getElementById('rgb').textContent = 'rgb('+r+','+g+','+b+')';
document.getElementById('hsl').textContent = 'hsl('+h+','+s+'%,'+l+'%)';
document.getElementById('swatch').style.background = hex;
}
The HSL math looks dense but is just normalizing the RGB cube. Trace it once and you'll never need to look it up again.
Putting It All Together
Every tool here:
- Runs entirely in the browser — nothing leaves your machine
- Requires zero build steps or npm installs
- Takes under an hour to build and deploy to GitHub Pages
The full versions (with nicer UI) live at profiterole-blog/tools. Fork any one, strip the CSS, and you have a zero-dependency internal toolbox in minutes.
Building a variation or adding a sixth tool? Drop it in the comments.
Top comments (0)