# 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 October 30, 2026

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:

```javascript
// 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:

```bash
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:

```text
 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

- [Debounce](https://www.ask-codey.com/glossary/debounce)
- [Closure](https://www.ask-codey.com/glossary/closure-javascript)
- [AbortController](https://www.ask-codey.com/glossary/abortcontroller)
- [textContent](https://www.ask-codey.com/glossary/textcontent)
- [createElement()](https://www.ask-codey.com/glossary/createelement)
- [Vitest](https://www.ask-codey.com/glossary/vitest-unit-testing)

## Related

- [Debounce and Throttle](https://www.ask-codey.com/lessons/javascript-performance-debounce-and-throttle)
- [Keeping Pages Safe: XSS and innerHTML](https://www.ask-codey.com/lessons/javascript-security-xss-and-innerhtml)
- [Put Your App Online with GitHub Pages](https://www.ask-codey.com/lessons/javascript-shipping-github-pages)
- [Testing Edge Cases, Errors and Async Code](https://www.ask-codey.com/lessons/javascript-testing-edge-cases-errors-async)

Source: https://www.ask-codey.com/lessons/exercise-javascript-bookstore-search
