Exercise: Bookstore Search

Make a bookstore's search box fast and safe, writing a debounce function tested with fake timers, a search that finds books by title or author, and results drawn with textContent, so a title made of HTML shows as text.

Freelancer · JavaScript · Web Development · Updated

Inkwell & Oak Books has 500 books and a search box on its website, and the search box has two problems. It's wasteful: every key you press sends a search to the catalog server, six of them for the word harbor. And it isn't safe: one seller listed a book whose title is <img src=x onerror=alert(1)>, and the page turns it into real HTML. You'll fix both in a Vite project: a debounce function so the page searches only when the shopper pauses, a matchBooks function that finds the books, and a renderResults that shows every title as text. Vitest tests check each one, and a short checklist checks the page.

Get the exercise code

In the Exercise box beside the lesson, choose Get the starter on GitHub to open its GitHub repository. Click Code, then Download ZIP, and unzip it where you keep your practice folders. Or clone it with Git.

The folder is a Vite project:

  • index.html, styles.css: the page and its look. Ready to use.
  • src/books.json: the store's catalog of 500 fictional books. Book 500 is the one with the HTML title. Ready to use.
  • src/slow-server.js: a pretend catalog server that answers each search after a wait, the way a real server does over a slow connection. A short search takes longer than a long one, as a long list of results would. Ready to use.
  • src/main.js: connects the page to your functions. It waits for a pause with your debounce, cancels an out-of-date search with an AbortController, and draws the answer with your renderResults. Ready to use, and worth reading.
  • src/search.js: debounce and matchBooks, for you to finish.
  • src/render.js: renderResults. It works, but it builds the results with innerHTML. For you to rewrite.
  • src/search.test.js, src/render.test.js: the Vitest tests. You don't need to change them.
  • package.json, package-lock.json, vite.config.js: the project's scripts, its tools pinned to exact versions, and Vite's settings. Ready to use.

Open the folder in VS Code, open a terminal there with Terminal > New Terminal, and install the tools once with npm install. Then npm run dev starts the page at http://localhost:5173.

What to do

0. See the problems first. Type harbor slowly into the search box, and watch Searches sent to the catalog go up with every key. Then type <img src=x onerror=alert(1)> into the box. An alert box pops up: the page turned your search into HTML and ran the code inside it. Here that's harmless (all it does is show a box), and it's how a real attack starts.

1. debounce(fn, ms), in src/search.js. It's declared, and for now it hands back fn unchanged, which is why every key searches. The comment above it says what it should do, with an example:

// 1. debounce(fn, ms)
//    Return a new function that waits until it hasn't been called for ms milliseconds, then calls fn once,
//    with the arguments of the last call.
//    const searchAfterPause = debounce(search, 300);
//    searchAfterPause('h'); searchAfterPause('ha'); searchAfterPause('harbor');
//    -> 300 ms after the last call, search('harbor') runs, once. search('h') and search('ha') never run.
export function debounce(fn, ms) {
  // For now it hands back fn itself, so every keystroke searches. Watch the counter on the page.
  return fn;
}

2. matchBooks(books, query), in the same file. It gives back the books whose title or author contains the search, ignoring capital letters and the spaces around the search. An empty search finds nothing.

3. renderResults(list, books, query), in src/render.js. Rewrite it so it builds each result with createElement and fills in every piece of text with textContent, with no innerHTML at all. The comment above it lists the elements and classes each result needs, and the message to show when nothing matches.

Check your work

Run the tests from the terminal:

npm test

At first 13 of the 15 tests fail. That's expected: each function you finish turns its tests green. The two that pass check things the starter's renderResults already does right (one item per book, and drawing the list fresh each time). When a test fails, its message says what it expected, for example Six quick calls make one call: clear the old timer each time.

The debounce tests use Vitest's fake timers (Testing Edge Cases, Errors and Async Code), so a 300 ms wait takes no time at all. The renderResults tests run on a pretend page made by jsdom. npm run test:watch runs them again each time you save.

Then try the page with npm run dev, and work through the checklist in the folder's README.md: a search for harbor sends one search, not six; a quick change of mind never shows old results; and book 500's title shows as text, with no alert box.

Hints

  • The timer ID has to outlive each call. Where can a variable live so that every call of the function debounce returns sees the same one? Debounce and Throttle builds this step by step.
  • Two lines do most of the work. Cancel the timer from the call before, then start a new one. clearTimeout does nothing when there's no timer yet, so the first call needs no special case.
  • Hand every argument on. (...args) => collects them all, and fn(...args) passes them all to fn.
  • matchBooks is one filter. Tidy the search once at the start with trim and toLowerCase, then compare with includes (String Methods).
  • Empty the list, then build. list.replaceChildren() empties it. Keeping Pages Safe: XSS and innerHTML shows how to build one result from several elements.

When you're done

Every test is green, and npm test ends with:

 Test Files  2 passed (2)
      Tests  15 passed (15)

and every box on the checklist is ticked.

Now open src/main.js and find the line controller?.abort();. What would a shopper see if it weren't there? The README.md has a way to find out.

So what

You've made a search box that's kind to the server, never shows an out-of-date answer, and can't be turned against its visitors. Those are the three things a code reviewer checks first in any search-as-you-type feature, and you can build every one of them yourself.

Key terms

Related