Case Study

Verbalize

3,000+ words LCP p50 ≈ 1.17s (Fast 4G + 4× CPU) TTFB 28–69ms

Most vocabulary tools I used for GRE and IELTS prep felt like digital chores: long lists you were meant to power through and somehow remember. That’s not how these words show up in real life. You meet them in short bursts, in context, usually while doing something else.

Verbalize is my attempt to match that reality. Instead of handing you a giant deck, it gives you a steady trickle of exam‑grade words, lets you mark the ones that matter, and quietly brings them into places you already visit—like a new browser tab.

Under the hood it’s a small system: a browser UI, an API running at the edge, and a simple extension surface. The interesting part isn’t the stack so much as the shared understanding of what “a word” is and how a user relates to it.


Starting from Shared Definitions

Very early, I decided not to let each part of the system invent its own concept of a word. Instead, I defined a shared schema that both the API and the clients import:

type Dataset = "gre" | "ielts";

type Word = {
  id: string;
  term: string;
  definition: string;
  examples: string[];
  dataset: Dataset;
};

This kind of type lives in a small shared package. The API uses it to validate and return data; the React app and extension use it to drive UI. Adding something like a difficulty tag or an extra example means updating the shared type once and then following the type errors to the places that care.

Conceptually, the architecture looks like this:

Client UI & Extension → Typed API Layer → Word Store

That simple shape keeps both sides honest. There’s one “give me a word” procedure, not three slightly different versions spread across the codebase.


A Discovery Flow Instead of a Deck

The main experience is a discovery view. You pick an exam track (GRE or IELTS), and the app surfaces one word at a time with its definition, example sentence, and a few extra details. From there you can save it, mark it as important, or just move on.

I wanted this to feel more like a daily routine than a dashboard. The app remembers your chosen dataset locally so you don’t have to keep re‑selecting it. A small bit of logic decides what to show next—either a deep‑linked word, a search result, or a random one from the chosen pool.

On the client, I lean on a typed API client and data‑fetching hooks instead of manual fetch calls. A simplified version:

function useRandomWord(dataset: Dataset) {
  return useQuery({
    queryKey: ["randomWord", dataset],
    queryFn: () => api.words.random({ dataset }),
  });
}

The real implementation does more (error states, retries, stale‑while‑revalidate), but the shape is the same: the UI asks for “a random word from this dataset” and doesn’t have to know about URLs or JSON structures.

Pronunciation is handled with a small custom hook that owns audio playback state, so the card component can stay focused on layout and interaction. That keeps concerns separated: audio as behavior, card as presentation.


Making “Important” Mean Something

I didn’t want “important” to be just a star icon you tap and forget about. It needed to be a signal the system could actually use.

When you mark a word as important, Verbalize updates two things:

  • your own status for that word, and
  • a global count that powers a simple “popular words” view.

Both updates are optimistic in the UI and then confirmed by the API. Other parts of the app stay in sync by listening for a small event when a word’s status changes, rather than relying on a heavy global store for everything. The backend remains the source of truth, but the front‑end doesn’t feel laggy or out of date.

This creates a gentle feedback loop: words you and others care about become easier to revisit, without forcing you into detailed study plans or flashcard regimes.


Keeping the UI Calm and Consistent

Visually, I wanted Verbalize to get out of the way. The UI is built from a small design system:

  • a set of tokens for colors and typography,
  • a few reusable components for cards, buttons, and layout, and
  • some micro‑animations for transitions so state changes feel clear but not noisy.

Accessibility is addressed through small, practical decisions: skip links for keyboard users, aria-labels on icon‑only buttons, and layouts that adapt between desktop and mobile without breaking reading flow. None of it is dramatic, but it prevents the app from feeling fragile as it grows.


Extending into a New Tab

Once the main app felt solid, I built a browser extension that turns the new tab page into a lightweight Verbalize surface.

The extension reuses the same word model, typed API client, and UI pieces as the main app. It shows a single word from your chosen dataset, with the same card and actions, in a simplified layout that’s mindful of new‑tab constraints.

Because the shared types and components already existed, the extension was mostly wiring and packaging. It doesn’t introduce new concepts; it just brings the same experience into a place you see many times a day, which quietly helps with spaced repetition.


What I Learned

Building Verbalize reinforced a few straightforward ideas:

  • Shared definitions for core concepts (“word”, “dataset”, “interaction”) remove a lot of friction.
  • It’s easier to extend a system when the front‑end and backend speak the same typed language from the start.
  • A small, focused feature set can still feel helpful if it respects how people actually study.

Verbalize isn’t trying to be the definitive exam prep platform. It’s a narrow tool that shows you one useful word at a time, in places you already are, and lets you quietly mark the ones that matter. For this project, that felt like the right distance between “serious studying” and “one more tab you don’t dread opening.”