JavaScript Crash Course in 10 Minutes (Build a Real App)

1 views

You can learn modern JavaScript from a list of syntax rules, or you can learn it by building something that calls a real API and breaks in real ways. This crash course takes the second route: a weather web app that looks up any city, shows a live 7-day forecast and compares five cities in parallel, using the free Open-Meteo API. It’s plain modern JavaScript with no framework and no build step: ES modules in the browser, Node 22 for quick experiments, and http-server to serve the files. This written version follows the video’s 13 chapters and adds the reasoning, gotchas and practice ideas around each one.

⚡ TL;DR – Default to const, use let when you must reassign, never var. Compare with ===. – map, filter, reduce, find and some replace most hand-written loops. – fetch doesn’t reject on HTTP 404 or 500. Check response.ok yourself. – Promise.allSettled lets five requests run in parallel without one failure sinking the rest. – Closures and the event loop explain most “weird” JavaScript behaviour.

Before you start

  • Node.js 22 (for running snippets) and a modern browser.
  • npx http-server to serve the folder. ES modules don’t load from file:// URLs.
  • No API key: Open-Meteo is free for non-commercial use (credit “Weather data by Open-Meteo.com”).
text
weather/
  index.html
  app.js        # DOM + events
  api.js        # fetch helpers
  format.js     # pure functions

Step 1: Variables and types

js
const city = 'Chennai';     // can't be reassigned
let temp = 31.5;            // can be
console.log(typeof city, typeof temp, typeof null);   // string number object (a famous quirk)
console.log('5' + 1, '5' - 1);   // "51" 4  — coercion
console.log(0 == '', 0 === ''); // true false

== coerces types before comparing, which produces surprises like 0 == ''. Use === everywhere and you’ll never need the coercion table.

Step 2: Functions, arrows, defaults and template literals

js
function toF(c) { return c * 9 / 5 + 32; }
const round = (n, digits = 1) => Number(n.toFixed(digits));
const label = (name, t) => `${name}: ${round(t)}°C`;

Arrow functions are shorter and don’t have their own this, which is usually what you want for callbacks. Default parameters replace the old x = x || 1 pattern, which broke when x was legitimately 0.

Step 3: Arrays, objects, destructuring, rest and spread

js
const day = { date: '2026-10-30', max: 31, min: 24, rain: 40 };
const { max, min, ...rest } = day;          // rest = { date, rain }
const days = [day, { ...day, date: '2026-10-31', max: 29 }];
const [today, tomorrow] = days;

Spread copies are shallow: nested objects are still shared. When updating state, copy each level you change.

Step 4: map, filter, reduce, find, some

js
const highs = days.map(d => d.max);
const rainy = days.filter(d => d.rain >= 50);
const avg = highs.reduce((sum, t) => sum + t, 0) / highs.length;
const firstHot = days.find(d => d.max > 30);
const anyRain = days.some(d => d.rain > 0);
console.log(firstHot?.date ?? 'none');   // optional chaining + nullish coalescing

?. stops at null or undefined instead of throwing, and ?? supplies a default only for those two values (unlike ||, which also replaces 0 and '').

Step 5: HTML and modules

html
<form id="search"><input name="city" required><button>Search</button></form>
<p id="status" aria-live="polite"></p>
<table id="forecast"></table>
<script type="module" src="app.js"></script>

type="module" gives you import/export, strict mode by default, and deferred execution, so the DOM exists when the script runs.

Step 6: import and export

js
// format.js
export const fmtDay = iso => new Date(iso).toLocaleDateString(undefined, { weekday: 'short' });
export default function toF(c) { return c * 9 / 5 + 32; }

// app.js
import toF, { fmtDay } from './format.js';

In the browser, include the .js extension in import paths. There’s no bundler to guess it for you.

Step 7: fetch with async/await

js
// api.js
export async function geocode(name) {
  const r = await fetch(`https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(name)}&count=1`);
  if (!r.ok) throw new Error(`Geocoding failed: ${r.status}`);
  const data = await r.json();
  if (!data.results?.length) throw new CityNotFoundError(name);
  return data.results[0];
}

export async function forecast({ latitude, longitude }) {
  const q = new URLSearchParams({
    latitude, longitude, timezone: 'auto',
    daily: 'temperature_2m_max,temperature_2m_min,precipitation_probability_max',
  });
  const r = await fetch(`https://api.open-meteo.com/v1/forecast?${q}`);
  if (!r.ok) throw new Error(`Forecast failed: ${r.status}`);
  return r.json();
}

The key gotcha: fetch only rejects on network failure. A 404 or 500 resolves normally, so always check r.ok.

Step 8: Promises and error handling

js
export class CityNotFoundError extends Error {
  constructor(city) { super(`No city called "${city}"`); this.name = 'CityNotFoundError'; }
}

async function show(name) {
  status.textContent = 'Loading…';
  try {
    const place = await geocode(name);
    render(place, await forecast(place));
    status.textContent = '';
  } catch (e) {
    status.textContent = e instanceof CityNotFoundError ? e.message : 'Something went wrong. Try again.';
    console.error(e);
  } finally {
    button.disabled = false;
  }
}

A custom error class lets you show a friendly message for expected failures and a generic one for everything else. finally runs on both paths, which is exactly where cleanup like re-enabling a button belongs.

Step 9: DOM and events

js
const form = document.querySelector('#search');
form.addEventListener('submit', e => {
  e.preventDefault();                      // stop the full page reload
  show(new FormData(form).get('city'));
});

function render(place, { daily }) {
  table.replaceChildren(...daily.time.map((t, i) => {
    const tr = document.createElement('tr');
    tr.textContent = `${fmtDay(t)}  ${daily.temperature_2m_max[i]}° / ${daily.temperature_2m_min[i]}°`;
    return tr;
  }));
}

Use textContent for data. innerHTML with API or user data is an XSS hole.

Step 10: Closures

js
function makeCache() {
  const store = new Map();               // private to the returned function
  return async (key, load) => {
    if (!store.has(key)) store.set(key, load());
    return store.get(key);
  };
}
const cached = makeCache();
const p = await cached('london', () => geocode('London'));

The inner function “closes over” store, keeping it alive and private. Caching the promise, not the result, also deduplicates two simultaneous requests for the same key.

Step 11: Classes

js
class Favorites {
  #items = new Set(JSON.parse(localStorage.getItem('fav') ?? '[]'));
  add(city) { this.#items.add(city); this.#save(); }
  remove(city) { this.#items.delete(city); this.#save(); }
  get list() { return [...this.#items]; }
  #save() { localStorage.setItem('fav', JSON.stringify(this.list)); }
}

#private fields really are private (not just a naming convention), and getters give you computed properties.

Step 12: Compare five cities with Promise.allSettled

js
const cities = ['Chennai', 'London', 'Tokyo', 'Nowhereville', 'Toronto'];
const results = await Promise.allSettled(cities.map(async c => {
  const place = await geocode(c);
  return { c, data: await forecast(place) };
}));
for (const r of results)
  console.log(r.status === 'fulfilled' ? `${r.value.c} ok` : `failed: ${r.reason.message}`);

Promise.all rejects as soon as one request fails and throws away the others. allSettled waits for all five and tells you which worked, which is what a comparison view needs. All five run in parallel, so the total time is roughly the slowest request, not the sum.

Step 13: The event loop

js
console.log('1 sync');
setTimeout(() => console.log('4 timer (macrotask)'), 0);
Promise.resolve().then(() => console.log('3 promise (microtask)'));
console.log('2 sync');

Synchronous code runs first, then all queued microtasks (promise callbacks), then the next macrotask (timers, events). That’s why a setTimeout(..., 0) still runs after an already-resolved promise, and why a long synchronous loop freezes the page: nothing else can run until it finishes.

Troubleshooting

  • “Cannot use import statement outside a module”: add type="module" to the script tag.
  • CORS or module errors when opening the file directly: serve the folder with npx http-server instead of double-clicking index.html.
  • The page reloads on submit: you forgot e.preventDefault().
  • undefined from an async function: you forgot await, or forgot to return inside .map(async ...).
  • The app shows old results: a slower earlier request finished after a newer one. Track the latest query and ignore stale responses, or use AbortController.

Best practices

  • Keep pure formatting functions separate from DOM code, so they’re easy to test.
  • Validate and handle every network failure path.
  • Prefer array methods and destructuring for readable data transformations.
  • Never put API or user data into innerHTML.

Edge cases: debouncing and cancelling requests

If you search as the user types, every keystroke fires a request, and responses can arrive out of order. Debounce the input and abort the previous request:

js
let controller;
let timer;
input.addEventListener('input', () => {
  clearTimeout(timer);
  timer = setTimeout(async () => {
    controller?.abort();                  // cancel the in-flight request
    controller = new AbortController();
    try {
      const r = await fetch(searchUrl(input.value), { signal: controller.signal });
      renderSuggestions(await r.json());
    } catch (e) {
      if (e.name !== 'AbortError') status.textContent = 'Search failed';
    }
  }, 300);
});

AbortError is expected here, so ignore it and report everything else. The same pattern prevents the “stale results” bug listed in Troubleshooting.

FAQ

Do I need a framework after this?

Not for small apps. Learn the platform first; React, Angular or Vue make more sense once you’ve felt the pain they solve.

Should I learn TypeScript next?

Yes, once these basics feel natural. TypeScript is JavaScript plus types, so everything here still applies.

Official documentation

Key takeaways

  • const, ===, array methods and destructuring cover most everyday code.
  • Check response.ok; fetch won’t do it for you.
  • Use custom errors plus try/catch/finally for clear failure handling.
  • Promise.allSettled for parallel work where partial failure is fine.
  • Understand the event loop and closures, and JavaScript stops being mysterious.

Watch: 100 Days of AI on YouTube

Short videos from CheatCoders, one AI engineering topic per day.

Open the playlist on YouTube · CheatCoders on YouTube

Deep-dive PDF

Get the expanded guide for this post — extra diagrams-style checklists, failure modes, and a production walkthrough. Free when you subscribe to CheatCoders.

PDF for this post is being prepared. Subscribe now and we’ll unlock downloads sitewide as packs go live.

Subscribe free →


Discover more from CheatCoders

Subscribe to get the latest posts sent to your email.

Comments

No comments yet. Why don’t you start the discussion?

Leave a comment

No account needed. Name and email are optional.