JavaScript & async — Practice
Vanilla JS + fetch of local JSON review data, AI-assisted (Session 2)
Course lectures and practices for JavaScript full‑stack web development with AI‑assisted workflows.
All AI prompts in this practice are starting points — feel free to reword them to match your style. The skill you are practising is not “copy the prompt”; it is verifying what the AI writes against the checklists below. You are the quality gate on the generated code.
Overview
Philosophy — build the core skills and skip the deep parts (React handles those better next session). You are writing vanilla JavaScript: DOM manipulation, events, and fetch with async/await. You are not writing React yet — that is Session 3’s job.
Goal — build a small vanilla‑JS review list prototype that:
- renders a list of reviews to the DOM,
- lets you filter it by source (Amazon / BestBuy / Walmart),
- loads the reviews from a local
reviews.jsonwithfetch+async/await(the same pattern you will later point at the:4000API), - (stretch) aggregates the reviews — per‑source counts and a rating histogram.
You verify each AI‑generated piece against a common‑mistakes checklist, then integrate it with a checkpoint and a git commit.
The AI‑first pattern (the rhythm of every task)
Every task below follows the same four steps:
| Step | What you do |
|---|---|
| Concept | 1–3 lines on what you are building and why it matters. |
| AI generates | Give the assistant the CRISP, copy‑paste prompt in the box. |
| You verify | Run it, then check it against the common AI mistakes for this task list. Fix what fails. |
| You integrate | Confirm the checkpoint (what “done” looks like) and make the git commit. |
If the AI’s output fails any item on a checklist, do not just copy it — tell the AI which item failed and ask it to fix only that. That back‑and‑forth is the skill.
Setup & running the app
This practice builds against the course’s real app, fullstack-minimal-app (see the course Introduction for the source). It has a React/Vite frontend (dev server on http://localhost:5173) and an Express backend on http://localhost:4000. The frontend calls the backend through the VITE_BACKEND_URL environment variable (default http://localhost:4000), and every API response is shaped { data: [...], meta: {...} }.
The product catalog this app ships is your given foundation — you are not building it in this session. Your deliverable is the review layer on top of it: a list of reviews, driven by a local reviews.json you fetch with the same fetch pattern you will later point at the :4000 API.
fetch will not work from a file:// page — open the prototype through the Vite dev server (http://localhost:5173), which serves anything under public/ at the site root, so a relative fetch('./reviews.json') resolves to a real URL. (When you later point the same fetch at the :4000 API, that call is cross‑origin and needs the backend running with CORS enabled.)
1. Get the app (if you have not already):
git clone https://github.com/ebpro/fullstack-minimal-app.git
cd fullstack-minimal-app2. Start the backend (context — the given catalog; not required for this session’s local‑JSON prototype, but useful to see what you are building on top of). Check the repo’s README for the exact script — the typical commands are:
cd fullstack-minimal-app/backend
npm install
npm run dev # or `npm start` — see the README3. Start the frontend dev server (Vite, on :5173):
cd fullstack-minimal-app/frontend
npm install
npm run dev # → http://localhost:51734. (Context) Peek at the given catalog. If you started the backend, run this in a terminal and confirm you see the product catalog JSON:
curl http://localhost:4000/api/productsYou should see an object with a data array (the given products) and a meta object. This catalog is your foundation, not your build target — this session’s prototype works on a local reviews.json, so the tasks below do not depend on the backend.
Your prototype files live in frontend/public/prototypes/. Vite serves anything under public/ at the site root, so frontend/public/prototypes/review-list.html is opened at http://localhost:5173/prototypes/review-list.html. Your local reviews.json sits beside it, so a relative fetch('./reviews.json') from the page resolves to it. No Live Server extension is needed.
RECALL / if needed — the canonical data shapes (the only ones in this course)
A product has exactly these fields:
| Field | Type | Example |
|---|---|---|
id |
number | 1 |
name |
string | "Wireless Bluetooth Headphones" |
description |
string | "Premium noise‑canceling headphones…" |
price |
number | 199.99 |
image_url |
string | "https://placehold.co/300x200?text=Headphones" |
category_id |
number | 1 |
category_name |
string | "Electronics" |
created_at |
ISO string | "2024-10-01T00:00:00Z" |
A review has exactly these fields:
| Field | Type | Notes |
|---|---|---|
review_id |
string | e.g. "AMZ_0001" — a string, not a number (propagates to S4/S5) |
source |
string | one of Amazon, BestBuy, Walmart |
author |
string | the reviewer’s name |
rating |
integer | 1–5 (there is no 0) |
title |
string | |
body |
string | the review text (the field is body, not content) |
created_at |
ISO string | |
verified_purchase |
boolean |
There is no “Helpful” button and no 0–5 rating anywhere in this course. If the AI adds either, that is a mistake to catch.
RECALL / if needed — async/await + fetch in 30 seconds
async function load() {
const res = await fetch('./reviews.json'); // a Promise<Response> (local file now; the :4000 API later)
if (!res.ok) throw new Error('HTTP ' + res.status); // 404/500 → throw, don't parse
const json = await res.json(); // a Promise<value>
return json; // a local file IS the array (no { data, meta } wrapper)
}await only works inside an async function. A fetch you forget to await gives you a Promise object — rendering that prints [object Promise].
RECALL / if needed — see the real response in DevTools
Open your page at http://localhost:5173/prototypes/review-list.html, press F12, go to the Network tab, select Fetch/XHR, and reload. Click the reviews.json request and read the Response pane. This is the ground truth your fetch code must match — use it whenever the AI’s code does not line up with the data that is actually served. (In later sessions the same tab shows the :4000 API responses instead.)
Task 1 — Render the review list from a local reviews.json
Concept. Before wiring a network API, prove you can turn data into pixels. Start from a local reviews.json (an array of reviews using the canonical review schema) and render one card per review into the DOM. Fetching a local file with fetch is the exact same pattern you will later point at the :4000 API — that is the whole point of starting local.
AI generates
Copy‑paste this prompt to your assistant:
Create three files for a vanilla-JS review-list page (no framework, no React, no Tailwind):
1. frontend/public/prototypes/reviews.json
- an array of 6 reviews, each with EXACTLY these fields:
review_id (STRING, e.g. "AMZ_0001"), source (one of "Amazon"/"BestBuy"/"Walmart"),
author (string), rating (integer 1-5), title (string), body (string),
created_at (ISO string), verified_purchase (boolean)
- vary the sources and the ratings so the later filter/sort/aggregate tasks have data to work with
2. frontend/public/prototypes/review-list.html
- <h1>Multi-Source Review List</h1>
- a short <p> subtitle: "Amazon, BestBuy, Walmart — one list"
- an empty <div id="review-list">
- a <style> block with a simple responsive list (CSS grid or flexbox) and basic card styling
- <script src="review-list.js"></script> just before </body>
3. frontend/public/prototypes/review-list.js (a plain <script>, NOT an ES module)
- an async function loadReviews() that:
* const res = await fetch('./reviews.json')
* if (!res.ok) throw new Error('HTTP ' + res.status)
* return await res.json() // the local file IS the array (no { data, meta } wrapper here)
- a function renderReviews(list) that:
* clears #review-list first (set innerHTML = "")
* loops over list and appends one card per review using a template literal
* each card shows: source, author, rating as "N / 5", title, body, created_at,
and a "Verified purchase" badge only when verified_purchase is true
* uses review_id as the card's data-id attribute (remember: it is a STRING)
- on DOMContentLoaded: show "Loading reviews...", await loadReviews(), render, and wrap in try/catch
Keep it minimal: no filters, no sort, no backend.
You verify — common AI mistakes for this task
- Made‑up schema. The AI often invents fields like
helpful,helpful_votes,comment,reviewer_name, oravgRating. Keep only the canonical review fields (review_id, source, author, rating, title, body, created_at, verified_purchase). The text field isbody, notcontent/comment; the name isauthor, notreviewer_name. review_idtreated as a number.review_idis a string (e.g."AMZ_0001"). If the AI emits numeric ids or drops the prefix, that breaks the schema — and it propagates to later sessions.innerHTMLin a loop.list.innerHTML += card(or=inside the loop) so only the last review shows, or duplicates pile up on re‑render. Clear once, then append per item.- Runs before the DOM is ready. No
DOMContentLoadedguard →getElementByIdreturnsnulland the script throws. - Reads a
{ data, meta }wrapper that is not there. A local JSON file is just the array — do not readjson.datahere. (The:4000API wraps in{ data, meta }; a local file does not.) - Rating shown raw / a 0–5 scale. Show
ratingas"N / 5"; there is no 0 and no “Helpful” button anywhere in this course. - Tailwind / external CSS. Plain
<style>only.
You integrate
Checkpoint. Open http://localhost:5173/prototypes/review-list.html and you should see the reviews rendered as cards, each with source, author, a N / 5 rating, title, body, created_at, and a “Verified purchase” badge when verified_purchase is true.
git add frontend/public/prototypes/reviews.json frontend/public/prototypes/review-list.html frontend/public/prototypes/review-list.js
git commit -m "feat(prototype): render review list from local reviews.json"Task 2 — Filter the list by source
Concept. A multi‑source list is only useful if you can narrow it down. Add a source filter (Amazon / BestBuy / Walmart) — the canonical filter dimension for this app. This is where you practise Array.prototype.filter() and event listeners, the two skills that transfer directly into React next session.
AI generates
Copy‑paste this prompt to your assistant:
Update frontend/public/prototypes/review-list.js to add a source filter to the EXISTING review list
(keep the fetched `reviews` array as the source of truth):
1. In the HTML, add above #review-list:
- a <select id="source-filter"> with an "All sources" option (value "all")
plus one option per source: Amazon, BestBuy, Walmart
2. Add applyFilters() that:
* reads the selected source (value "all" = no filter)
* returns a NEW array: reviews.filter(r => source === "all" || r.source === source)
(do NOT mutate the original `reviews`)
* re-renders with renderReviews()
3. Wire the <select> to its 'change' event, calling applyFilters().
Do not introduce a backend or a second data source — keep filtering the array you already fetched.
You verify — common AI mistakes for this task
- Mutates the source.
reviews = reviews.filter(...)reassigns the original, so filtering twice keeps shrinking the list. Derive a new array from the untouched source each time. - Filters the wrong field. The canonical filter dimension is
source(Amazon / BestBuy / Walmart) — notcategory, notauthor. - Wrong event. The
<select>useschange. (A free‑text search input would useinput— that is not part of this task.) - Forgets to re‑render. The filtered array is computed but the DOM is not updated.
- Drops the “all” case. Selecting “All sources” must return the full list, not an empty one.
You integrate
Checkpoint. Choosing a source narrows the list live to that source’s reviews, and selecting “All sources” brings them all back.
git add frontend/public/prototypes/
git commit -m "feat(prototype): filter review list by source"Task 3 — Sort the list (by rating or by date)
Concept. Once you can filter, you can order. Add a sort control: by date (created_at, newest first) or by rating (highest first). This practises Array.prototype.sort() with a comparator and parsing ISO date strings — both carry straight into React next session.
AI generates
Copy‑paste this prompt to your assistant:
Update frontend/public/prototypes/review-list.js to add sorting on top of the existing source filter
(keep both working together):
1. In the HTML, add next to #source-filter:
- a <select id="sort-select"> with these options (value -> meaning):
"newest" -> created_at descending
"oldest" -> created_at ascending
"highest" -> rating descending
"lowest" -> rating ascending
2. Add sortReviews(list, mode) that returns a NEW sorted array (do NOT mutate `list`):
* "newest": [...list].sort((a, b) => new Date(b.created_at) - new Date(a.created_at))
* "oldest": [...list].sort((a, b) => new Date(a.created_at) - new Date(b.created_at))
* "highest": [...list].sort((a, b) => b.rating - a.rating)
* "lowest": [...list].sort((a, b) => a.rating - b.rating)
3. Update applyFilters() so the pipeline is: source filter -> sort -> renderReviews().
Wire the sort <select> to its 'change' event.
You verify — common AI mistakes for this task
- Mutates / sorts in place.
reviews.sort(...)sorts the original array in place and reorders your source of truth. Copy first ([...list]), then sort the copy. - Sorts the wrong field. Date sorts use
created_at(parse withnew Date(...)); rating sorts userating. Do not sort byreview_idorauthor. - Fragile date comparison. Comparing raw ISO strings “works” but
new Date(a) - new Date(b)is the robust form; a malformed date yieldsNaNand breaks the order. - Ignores the current filter. Sorting must run after the source filter, so the visible (filtered) list is what gets sorted.
- Forgets to re‑render. The sorted array is computed but the DOM is not updated.
You integrate
Checkpoint. The sort control re‑orders the (already filtered) list live — newest/oldest by created_at, highest/lowest by rating — and “All sources” plus a sort still shows the full list in that order.
git add frontend/public/prototypes/
git commit -m "feat(prototype): sort review list by rating and date"Task 4 (stretch) — The aggregate (per‑source counts + rating histogram)
Concept. Stretch goal that previews the “read a list and summarize it” skill you will keep using. Compute two aggregates over the currently visible (filtered + sorted) reviews: per‑source counts and a rating histogram (buckets 1–5). This is where you practise reduce — and guarding your data, because real reviews are not always well‑formed.
AI generates
Copy‑paste this prompt to your assistant:
Add an aggregate summary to frontend/public/prototypes/review-list.js (optional stretch):
1. Add aggregate(list) that returns { bySource, histogram } computed over the CURRENTLY VISIBLE
(filtered + sorted) list:
* bySource: { Amazon: n, BestBuy: n, Walmart: n } (counts of r.source)
* histogram: { 1: n, 2: n, 3: n, 4: n, 5: n } (counts of r.rating)
2. GUARD the rating: only count a review in the histogram when
Number.isInteger(r.rating) && r.rating >= 1 && r.rating <= 5.
Ignore anything else (0, 6, a string like "5", NaN, missing) — do not crash and do not add a
bucket outside 1..5.
3. Render a small summary block above #review-list:
- one line per source: "Amazon — 2"
- one line per rating bucket 1..5: "5 — 1" (word it as a count, not a star widget)
- recompute and update it inside applyFilters() so it always reflects the visible list.
You verify — common AI mistakes for this task
- No rating guard. A rating of
0,6,"5"(a string),NaN, or missing must be ignored — not counted, not added to a bogus bucket, and not crash the page. The guard isNumber.isInteger(r.rating) && r.rating >= 1 && r.rating <= 5. - Counts the wrong field.
bySourcecountsr.source; the histogram countsr.rating. Do not countreview_idorauthor. - Aggregates the wrong list. The summary must reflect the visible (filtered) list, so it updates when the source filter changes — not the full static array.
- Missing bucket. All five rating buckets (1–5) should appear even when a bucket is
0. - Recomputes in the wrong place. The aggregate must be recomputed in the filter/render pipeline (after filtering), not once on load.
You integrate
Checkpoint. The summary shows per‑source counts and a 1–5 rating histogram that update live as you change the source filter, and an invalid rating in the data is silently ignored (no crash, no bogus bucket).
git add frontend/public/prototypes/
git commit -m "feat(prototype): per-source counts + rating histogram (stretch)"Self‑check
Run through this before you finish:
Building toward Session 3
You just did, by hand, everything React is going to take over:
- Manual DOM rendering → React’s declarative components
- Event listeners you attach yourself → React’s event props
- A static array / a
fetchyou trigger onDOMContentLoaded→ a data‑fetching hook - Re‑rendering by clearing and rebuilding
innerHTML→ React’s diffing
Session 3 converts your review list into React components fed by the same sample data — the real backend arrives in Session 4. But that is tomorrow’s job. For now: vanilla JS, fetch, async/await, and the discipline of verifying what the AI wrote.