I never planned to build a word puzzle solver. But when I decided to build my own spelling bee solver, finishing it with simple code gave me “aha” moments.
The React Question Nobody Asked
My first instinct was to reach for React. That's what we do now, right? Building something for the web? React. Need a button? React. Making toast? Probably React.
But then I stopped and actually thought about what I was building. A honeycomb interface with seven letters. Click a button, get a list of words back. That's it. The entire app.
Did I really need React for this?
I decided to try something crazy: just write regular JavaScript. No framework. No build process. No node_modules folder eating up gigabytes of disk space.
You know what happened? The whole thing worked perfectly, and the JavaScript file ended up being 15KB. My last React project? The bundle was over 200KB before I even wrote any actual features.
Making Keyboard Input Feel Natural
The tricky part wasn't the logic. Finding valid words is straightforward once you have a dictionary to check against. The hard part was making it feel responsive.
I wanted people to be able to just start typing. Not click into an input field, not focus anything, just type and see the letters appear in the honeycomb.
Here's the pattern I used:
document.addEventListener('keydown', (e) => {
const key = e.key.toUpperCase();
if (key.length === 1 && /[A-Z]/.test(key)) {
addLetterToDisplay(key);
}
if (e.key === 'Backspace') {
removeLastLetter();
}
});
function addLetterToDisplay(letter) {
const display = document.getElementById('letterDisplay');
if (display.textContent.length < 7) {
display.textContent += letter;
updateHoneycomb();
}
}
Nothing fancy, but it works. The user can just start typing and immediately see feedback. No hunting for input fields.
What really made the difference was handling all the edge cases. What happens when someone pastes text? What about mobile keyboards?
document.addEventListener('paste', (e) => {
e.preventDefault();
const pastedText = e.clipboardData.getData('text');
const letters = pastedText
.toUpperCase()
.replace(/[^A-Z]/g, '')
.slice(0, 7);
document.getElementById('letterDisplay').textContent = letters;
updateHoneycomb();
});
These little touches make it feel polished instead of janky.
The Word Search Algorithm
For finding valid words, I kept it simple. Sometimes brute force is fine if your dataset isn't huge.
function findValidWords(centerLetter, outerLetters) {
const allLetters = centerLetter + outerLetters;
const results = [];
for (const word of dictionary) {
if (word.length < 4) continue;
if (!word.includes(centerLetter)) continue;
const wordLetters = [...new Set(word.split(''))];
const isValid = wordLetters.every(letter =>
allLetters.includes(letter)
);
if (isValid) {
const isPangram = wordLetters.length === 7;
results.push({
word: word,
pangram: isPangram,
points: calculatePoints(word, isPangram)
});
}
}
return results;
}
function calculatePoints(word, isPangram) {
let points = word.length === 4 ? 1 : word.length;
if (isPangram) points += 7;
return points;
}
Could I optimize this with a trie or hash map? Sure. But for 50,000 words, this runs in under 100ms. Fast enough.
I learned that premature optimization is real. Build something that works first, then measure if it's actually slow.
Keeping The API Simple
Here's where things got interesting. The tool needed a backend, but I didn't want to overcomplicate it.
My PHP endpoint is basically:
<?php
header('Content-Type: application/json');
$center = strtoupper($_GET['center'] ?? '');
$outer = strtoupper($_GET['outer'] ?? '');
if (strlen($center) !== 1 || strlen($outer) !== 6) {
http_response_code(400);
die(json_encode(['error' => 'Invalid input']));
}
$dictionary = json_decode(file_get_contents('words.json'), true);
$results = [];
foreach ($dictionary as $word) {
if (isValidWord($word, $center, $outer)) {
$results[] = $word;
}
}
echo json_encode(['words' => $results]);
The Simple Security That Actually Works
I needed to prevent abuse without requiring user accounts. The solution? Multiple simple checks that work together.
One thing I did was add basic rate limiting using the filesystem instead of Redis:
$ip = $_SERVER['REMOTE_ADDR'];
$rateFile = sys_get_temp_dir() . '/rate_' . md5($ip . date('YmdH')) . '.txt';
if (file_exists($rateFile)) {
$count = (int)file_get_contents($rateFile);
if ($count > 100) {
http_response_code(429);
die(json_encode(['error' => 'Too many requests']));
}
file_put_contents($rateFile, $count + 1);
} else {
file_put_contents($rateFile, 1);
}
It's not perfect, but combined with a few other checks, it stops 99% of abuse. And it requires zero database setup.
Displaying Results Without Framework Magic
When results come back from the API, I wanted to group them nicely. Pure JavaScript made this surprisingly clean:
function displayResults(words) {
const grouped = {};
words.forEach(word => {
const len = word.word.length;
if (!grouped[len]) grouped[len] = [];
grouped[len].push(word);
});
let html = '';
Object.keys(grouped).sort().forEach(length => {
const wordList = grouped[length];
html += `
<div class="word-group">
<h3>${length}-Letter Words (${wordList.length})</h3>
<div class="word-grid">
${wordList.map(w => `
<span class="${w.pangram ? 'pangram' : ''}">
${w.word} ${w.pangram ? '🐝' : ''}
</span>
`).join('')}
</div>
</div>
`;
});
document.getElementById('results').innerHTML = html;
}
No virtual DOM. No reconciliation. Just update the HTML when data changes. It's fast and easy to understand.
What Surprised Me Most
The whole project taught me something important: simple code is often better code.
When I need to fix a bug, I can just read the code and understand it. No digging through framework documentation. No wondering which lifecycle hook to use. Just plain JavaScript that does what it says.
And performance? The entire page loads in under 200ms. The API responds in about 60ms.
Turns out when you keep things simple, they tend to just work.
Things I'd Change
If I rebuilt this today, I'd probably add a service worker for offline support. Maybe use Web Workers for the dictionary search to keep the main thread free. Add some nice animations when results appear.
But honestly, I'm glad I kept the first version simple. It made it easy to get something working, and then easy to improve it later.
Every complex system starts as a simple one that worked. I think we forget that sometimes.
Why This Still Matters
The whole experience reminded me why I started coding. Not to configure webpack or manage package versions, but to make things that people actually use.
If you're working on a side project right now, maybe try building the simplest version that could work. You might be surprised how far vanilla JavaScript can take you.
Questions about any of this? Want to debate vanilla JS vs frameworks? Drop a comment, I genuinely enjoy talking about this stuff.
Top comments (0)