DEV Community

Cover image for Reverse-Engineering an Old Node.js Crossword App into a Modern Next.js Stack

Reverse-Engineering an Old Node.js Crossword App into a Modern Next.js Stack

As an IT undergrad at the University of Moratuwa, I’ve built my fair share of projects. Recently, I looked back at an old project of mine—a traditional Node.js Crossword App and realized it was time for a complete teardown.

The old app worked, but the architecture felt dated. I wanted to modernize it, improve the performance, and implement a cleaner, editorial-style UI. Instead of just refactoring, I decided to reverse-engineer the core logic of my own app and rebuild it from the ground up using Next.js 16, Neon PostgreSQL, Better Auth, and v0 by Vercel.

Here is how I broke down the legacy system and engineered the new application, Crosshatch. You can check out the live deployment here: crossword-web-app.vercel.app


The Teardown: Reverse-Engineering the Core Loop

When reverse-engineering an existing app—even your own—the goal is to separate the underlying business logic from the legacy plumbing. I ignored the old routing and view layers and focused entirely on the data structures and the game loop.

I identified three critical systems that needed to be extracted and modernized:

  1. Grid Generation: How words intersect and fit into a 10x10 matrix.
  2. State Management: Tracking user input, active cells, and directional navigation (Across vs. Down).
  3. Session & Progress: How to persist a user's progress without hammering the database.

Once I had the core logic mapped out, I started the rebuild.

Accelerating the UI with v0

I wanted the new app to have a calm, newspaper-inspired light theme. To move fast, I leveraged v0 by Vercel to generate the initial React components.

By prompting v0 with my required game state constraints (e.g., handling keyboard events like Tab, Shift+Tab, and arrow keys for navigation), I was able to rapidly prototype the 10x10 interactive grid. v0 handled the Tailwind CSS boilerplate, allowing me to focus on wiring up the complex state machine using a custom useCrossword React hook.

The Modern Stack & Architecture

With the UI taking shape, I built out a robust backend architecture to support it.

  • Framework: Next.js 16 (App Router)
  • Database: Neon PostgreSQL with Drizzle ORM
  • Authentication: Better Auth (handling secure email/password flows and session management)

Here are the key technical problems I had to solve during the rebuild:

1. Deterministic Puzzle Generation

In the old app, puzzle generation could be unpredictable. For the new build, I implemented a Seeded RNG algorithm. By using the puzzle's ID (e.g., animals-1) as the seed, the generation is completely deterministic. It shuffles the word list, places the first word at (0,0), and maps intersecting cells. If you and a friend both load animals-1, you are guaranteed to get the exact same layout.

Because generating a puzzle is CPU-intensive, I implemented a caching layer. The server generates the puzzle once, caches it in a Map, and serves it instantly on subsequent requests.

2. Smart Autosave & The Visibility API

Losing crossword progress is frustrating. I built a system that autosaves every 5 seconds. To prevent excessive database writes, I used a PostgreSQL UPSERT pattern via Drizzle:

INSERT INTO puzzle_progress (userId, puzzleId, entries, elapsedSeconds)
VALUES ($1, $2, $3, $4)
ON CONFLICT (userId, puzzleId)
DO UPDATE SET entries = $3, elapsedSeconds = $4, updatedAt = now()

Enter fullscreen mode Exit fullscreen mode

Additionally, the puzzle timer uses the browser's Visibility API. If you switch tabs, the timer automatically pauses, ensuring your "Solve Time" stats remain accurate to your actual active playtime.

3. Data Integrity: The "No Double-Credit" Problem

I built a user dashboard to track completions, best times, and average solve times. But what happens if a user submits the same completed puzzle twice?

Instead of writing complex application-level checks, I let the database handle it. I added a unique constraint on (userId, puzzleId) in the completion table and utilized the ON CONFLICT DO NOTHING clause. If a user solves a puzzle for the first time, it records their time. If they re-submit it, the database ignores the insert, returning a response that acknowledges the correct answers without skewing their dashboard analytics.

Final Thoughts

Reverse-engineering an older project and rebuilding it with modern tooling is one of the best ways to measure your growth as a developer. By moving to Next.js and leveraging v0 for rapid UI prototyping, I was able to spend my time solving actual engineering problems (like deterministic generation and state synchronization) rather than wrestling with basic CSS.

Check out the live app at crossword-web-app.vercel.app and let me know what you think of the architecture! I’m always open to technical feedback.

Top comments (0)