Every other calculator-style tool on RedoHub, including the CGPA calculator, runs entirely in the browser with no server call. The Hash Generator is the one exception, and the reason comes down to a single algorithm: bcrypt.

Bcrypt Doesn't Belong in a Browser Tab

MD5, SHA-1, SHA-256, and SHA-512 are all things the browser's own SubtleCrypto API can compute directly, no server required. Bcrypt is different -- it's deliberately slow, with a configurable "work factor" that's supposed to make brute-forcing expensive, and it isn't part of any browser's built-in crypto API. Implementing it correctly in JavaScript means either shipping a sizeable bcrypt library to every visitor or reimplementing the algorithm myself, and getting a security-sensitive algorithm subtly wrong in a from-scratch reimplementation is exactly the kind of risk I didn't want to take. Instead, every hash request goes to a small backend endpoint:

const response = await fetch(`${API_BASE_URL}/generate-hash/${selectedAlgorithm}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ input: inputText }) });

That one architectural choice -- push the actual hashing to a server that already has a battle-tested bcrypt implementation -- meant I didn't have to reinvent password-hashing cryptography just to offer it as a feature. It also means the same endpoint can compute MD5 and SHA-256 too, even though the browser could technically do those locally; keeping every algorithm behind one consistent API is simpler than splitting the logic between "algorithms the browser can do" and "algorithms it can't."

Defaulting to bcrypt Was a Deliberate Nudge

Look at the algorithm selector and bcrypt is the button that's already active on page load, not MD5 or SHA-256:

<button class="algorithm-btn active" data-algorithm="bcrypt">bcrypt</button> <button class="algorithm-btn" data-algorithm="md5">MD5</button>

A tool like this gets used by two very different kinds of visitors: people who genuinely need bcrypt for password storage, and people who just want a quick MD5 checksum for a file and don't know or care which algorithm is "correct." I could have defaulted to MD5 since it's the most commonly searched term, but MD5 and SHA-1 are exactly the algorithms you should not use for passwords, and I didn't want the tool's own default to model bad security practice to someone who doesn't know better yet. Making bcrypt the default costs nothing for the checksum use case -- they just click a different button -- but it means a beginner who doesn't switch anything gets the secure choice by accident instead of the insecure one.

Escaping Before It Ever Touches innerHTML

The generated hash comes back from the API as a plain string and gets rendered straight into the page with innerHTML. Template literals plus innerHTML is a classic XSS opening if the value being inserted isn't trustworthy, and while a hash value should never contain HTML, "should never" isn't a security guarantee -- an API response could be tampered with, or a future bug could pass the wrong string through. So every value that reaches the DOM goes through an explicit escape first:

function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; }

It's a small trick -- setting textContent then reading back innerHTML forces the browser to HTML-encode anything unsafe -- but it means the one function on the page that renders untrusted-ish content never has to trust its input. Given the entire point of this tool is security, having its own rendering logic be sloppy about injection would have been an embarrassing contradiction.

The Toast System Is the Same Pattern as the CGPA Calculator

I noticed while building this that the "fade an element out, then remove it after a timeout that matches the CSS animation duration" pattern I used for the CGPA calculator's row removal showed up again here, almost unchanged, for toast notifications:

setTimeout(() => { toast.style.animation = 'slideInRight 0.3s ease-out reverse'; setTimeout(() => toast.remove(), 300); }, 3000);

Neither tool shares any code with the other -- they're separate pages with separate script files -- but I ended up reaching for the identical nested-setTimeout shape both times because it's the simplest way to sequence "show something, then animate it out, then actually remove it" without a proper animation library. It's a small tell that a few UI conventions have quietly become house style across RedoHub's tools, even without a shared component system enforcing it.

One Enter Key, Two Different Behaviors

The plain-text textarea for generating a hash listens for Ctrl+Enter, while the single-line "plain text" field in the verification section listens for plain Enter:

document.getElementById('inputText').addEventListener('keypress', function (e) { if (e.key === 'Enter' && e.ctrlKey) generateBtn.click(); }); document.getElementById('plainText').addEventListener('keypress', function (e) { if (e.key === 'Enter') checkBtn.click(); });

That's not an inconsistency, it's intentional: a <textarea> needs plain Enter to insert a newline, since the text people hash is sometimes multi-line, so submitting on plain Enter there would make it impossible to hash anything with a line break. A single-line <input> has no such need, so Enter can safely mean "submit" there. It's a tiny detail, but getting it backwards would have quietly broken multi-line input for a tool whose entire job is accepting arbitrary text.

What I'd Change Next

The one thing I'm not fully happy with is that the API base URL is hardcoded as a plain constant at the top of script.js, pointing at app.redohub.com/api. It works fine because that endpoint is stable, but it means switching environments (say, for local testing against a staging API) requires editing and re-deploying the JavaScript file rather than reading an environment-specific config value. For a tool this size it hasn't been worth building a config layer around, but it's the kind of shortcut that starts to hurt the moment there's a second environment to support.