Async JavaScript & HTTP Requests

A quick reference for asynchronous JavaScript — sync vs async, the event loop, promises, async/await, and making HTTP requests to APIs.

Synchronous vs Asynchronous

Term Description
Synchronous Executes top to bottom, line by line. Each line BLOCKS until the previous one finishes — like laying bricks for a foundation, you can't skip a level.
Asynchronous Code that can run in parallel to other code already running, without waiting for it to finish first — NON-blocking. Like preheating an oven while prepping ingredients at the same time.
Thread A resource a computer gives a program to carry out a task. One thread generally completes one task at a time; more threads = more tasks running concurrently. Modern multithreading mostly comes from multi-core CPUs.
Why this matters for the browser:

Synchronous (blocking) code in the browser stops the user from
interacting with the page until it finishes — a bad experience if
something takes a while (e.g. waiting on a slow API).

Asynchronous code lets only the necessary code load up front, while
slower operations (API calls, database queries, anything with an
unpredictable wait time) run in the background — the user can keep
interacting with the page the whole time.
                    

Async JavaScript

JavaScript is a SINGLE-THREADED language — only one task at a time. It still achieves async behaviour through the EVENT LOOP (see below), not through true multithreading.

Syntax Description Example
Async callback A function that runs only once a specific condition is met (e.g. a click), concurrently with whatever else is running. el.addEventListener('click', cb)
setTimeout(cb, ms) Runs cb ONCE after at least ms milliseconds. Non-blocking — code after it keeps running immediately. setTimeout(() => {}, 1000)
setInterval(cb, ms) Runs cb REPEATEDLY, every ms milliseconds. Also non-blocking. setInterval(() => {}, 300000)
setTimeout(() => {
  console.log('Delay the printing of this string, please.');
}, 1000);
console.log('Doing important stuff.');
console.log('Still doing important stuff.');

// Output, in this order:
// 'Doing important stuff.'
// 'Still doing important stuff.'
// 'Delay the printing of this string, please.'
The setTimeout() callback runs LAST even though it's written FIRST —
proof that it doesn't block the lines after it. The two synchronous
console.log() calls run immediately; the delayed one waits its turn.
                    

Concurrency Model & Event Loop

JavaScript can't run two statements at the exact same time (single thread) — but it can EMULATE concurrency using the event loop, rather than true parallel execution.

// BLOCKING example — a long-running loop delays the next line for real:
console.log("I'm learning about");
for (let idx = 0; idx < 999999999; idx++) {}
console.log("the Event Loop");
// There's a real, noticeable PAUSE between the two logs while the
// loop runs — this is genuinely blocking code, not just instant output.
// NON-BLOCKING example — setTimeout lets later lines run first:
console.log("I'm learning about");
setTimeout(() => { console.log("Event Loop"); }, 2000);
console.log("the");

// Output order: "I'm learning about" -> "the" -> "Event Loop"
// (the 2 second delayed log prints LAST, after the synchronous line)
Part Role
Heap Unordered block of memory where variables/objects currently in use are stored.
Call Stack Tracks which function is currently running. LAST IN, FIRST OUT (LIFO) — calling a function pushes a frame on top; finishing pops it off. The global execution context sits at the very bottom.
Node / Web APIs Handle async operations (timers, network requests, DOM events) outside the call stack, then hand the callback back once ready.
Event Queue List of callback functions waiting to re-enter the call stack. FIRST IN, FIRST OUT (FIFO). No code executes here — it's just a waiting line.
Event Loop Continually checks: is the call stack empty? If yes AND something is waiting in the event queue, push it onto the stack to run.
Event loop walkthrough — tracing this code:

console.log("This is the first line of code in app.js.");

function usingsetTimeout() {
    console.log("I'm going to be queued in the Event Loop.");
}
setTimeout(usingsetTimeout, 3000);

console.log("This is the last line of code in app.js.");

1. First console.log() is pushed onto the stack, runs, pops off.
2. setTimeout() is pushed onto the stack.
3. setTimeout() hands its callback (usingsetTimeout) to a Web API,
   which starts a 3-second timer, then setTimeout() itself pops off
   the stack — the WAITING happens outside the stack, not on it.
4. Last console.log() is pushed onto the stack, runs, pops off.
5. Stack is now empty. Meanwhile the event loop has been polling it.
6. After 3 seconds, usingsetTimeout is placed in the event queue.
7. Event loop sees the stack is empty and pushes usingsetTimeout onto it.
8. Its console.log() runs, then usingsetTimeout pops off the stack.

Key insight: the stack being EMPTY is the trigger that lets the event
loop hand off the next queued callback — that's why synchronous code
always finishes first, no matter how the async code is ordered on the
page.
                    
Preview — full Promise syntax covered in the Promises section below:

const shopForBeans = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('2. Bought some beans.');
      resolve('black');
    }, 1000);
  });
};

async function getBeans() {
  console.log('1. Heading to the store...');
  let value = await shopForBeans();
  console.log(`3. Making ${value} beans for dinner!`);
}

getBeans();
console.log('Also logs — runs before the Promise resolves.');

// Order: 1 -> "Also logs..." -> (1 second passes) -> 2 -> 3
// await pauses only INSIDE getBeans() while it waits on the Promise —
// it does NOT block code outside that function, same event loop rules
// as setTimeout above.
                    

Promises

A Promise is an object representing the EVENTUAL outcome of an asynchronous operation — like a dishwasher: it's running (pending), then either finishes with clean dishes (fulfilled) or fails because there was no soap (rejected).

State Description
Pending Initial state — the operation hasn't completed yet.
Fulfilled Operation succeeded. The promise has a RESOLVED value.
Rejected Operation failed. The promise has a REJECTION reason (usually an Error).
Settled Umbrella term meaning no longer pending — either fulfilled OR rejected.
const executorFunction = (resolve, reject) => {
  if (someCondition) {
    resolve('I resolved!');
  } else {
    reject('I rejected!');
  }
};

const myFirstPromise = new Promise(executorFunction);
// Calling .then()/.catch() on a promise stored in its own variable,
// rather than chaining straight off a function call — same rules apply,
// .then()/.catch() are called ON the promise variable, never the
// other way around:
const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    const resultOfAnAsyncOperation = true;
    if (resultOfAnAsyncOperation) {
      resolve('Operation Successful!');
    } else {
      reject(Error('Promise Rejected'));
    }
  }, 1000);
});

promise.then((res) => {
  console.log(res);
}, (err) => {
  alert('Operation Failed!');
});

// promise.catch((err) => { console.log(err); });   would work the same
// way for rejection-only handling — promise.catch(...), NOT catch.promise(...)
Constructing a promise:

new Promise(executorFunction) — note the capital P, like Error/Array/Date,
it's a built-in constructor (easy typo: writing `promise` lowercase).

The Promise CONSTRUCTOR takes only ONE argument: the executor function
itself. Don't confuse this with the executor FUNCTION's own arity —
that function separately takes two parameters of its own (commonly
named resolve, reject). These are two different arities for two
different things:
  new Promise(executorFunction)        ← constructor takes 1 argument
  (resolve, reject) => { ... }         ← executor function takes 2 params

Which parameter is resolve and which is reject is determined by
POSITION, not by what you name them — the FIRST parameter is always
the success callback, the SECOND is always the failure callback, even
if you named them function1/function2 instead:
  new Promise((function1, function2) => {
    function1('success');  // this ALWAYS means resolve, regardless of name
  });

The executor function runs automatically and receives resolve/reject as
its two parameters — these are NOT written by you, JavaScript supplies
them. Calling resolve(value) sets the promise to fulfilled with that
value; calling reject(reason) sets it to rejected with that reason.

Producer vs consumer:
  library.js — PRODUCES the promise (owns resolve/reject decision logic)
  app.js     — CONSUMES the promise (.then()/.catch(), never sees the
               new Promise(...) machinery directly)
                    
Syntax Description
.then(onFulfilled, onRejected) Attach handlers — called with the settled value once the promise settles. Both arguments optional. .then() ALWAYS returns a promise, even if a handler is missing (passes the same settled value through).
.catch(onRejected) Shorthand for .then(null, onRejected) — cleaner way to separate failure handling from success handling (separation of concerns).
typeof aPromise Returns 'object', NOT 'Promise'. Same as arrays — typeof only distinguishes primitives vs object, it doesn't name the specific class/constructor.
Promise.all([...]) Runs multiple promises CONCURRENTLY. Fulfills only once ALL resolve (with an array of their values); rejects immediately if ANY one rejects.
checkInventory(order)
  .then((resolvedValueArray) => {
    return processPayment(resolvedValueArray);
  })
  .then((resolvedValueArray) => {
    return shipOrder(resolvedValueArray);
  })
  .then((successMessage) => {
    console.log(successMessage);
  })
  .catch((errorMessage) => {
    console.log(errorMessage);
  });
Promise chaining (composition) — the rule that makes it work:

Each .then()'s success handler must RETURN the next promise. If you
don't return it, the NEXT .then() in the chain just receives the
ORIGINAL promise's settled value instead — and this fails SILENTLY,
no error thrown, which makes it a nasty thing to debug.

The settled value passed into a handler is always ONE argument. If a
promise resolves with an array like [order, total], that whole array
IS the single argument — e.g. processPayment(resolvedValueArray), not
processPayment(order, total). Don't try to unpack it into separate
function arguments unless the next function explicitly destructures it.

.then(onFulfill, onReject) and .then(onFulfill).catch(onReject) produce
the SAME output on rejection — because .then(onFulfill) with no second
argument just passes a rejection straight through unchanged to whatever
.catch() comes next:

promise.then(onFulfill, onReject);          // same result on reject as:
promise.then(onFulfill).catch(onReject);    // this — onFulfill is only
                                             // ever called on success
                                             // either way
                    
Two common mistakes:

1. NESTING instead of chaining — still works, but unreadable, and gets
   worse with every additional promise:

   returnsFirstPromise().then((val) => {
     return returnsSecondValue(val).then((val2) => {
       console.log(val2);          // nested .then() inside a .then()
     });
   });

   Fix — flatten it by returning and chaining instead:

   returnsFirstPromise()
     .then((val) => returnsSecondValue(val))
     .then((val2) => console.log(val2));

2. FORGETTING to return a promise inside a handler — same silent
   failure as above, just from omission rather than nesting:

   .then((val) => {
     returnsSecondValue(val);      // missing `return` — chain breaks silently
   })
                    
Watch out — Future Alan's repeat offender this lesson:

Only the LAST line in a chained sequence gets a semicolon. Every line
before it must be semicolon-free, or the chain breaks:

checkInventory(order);    // semicolon here ends the statement —
  .then(handleSuccess);   // this becomes an orphaned, broken line

checkInventory(order)     // correct — no semicolon, chain continues
  .then(handleSuccess)
  .catch(handleFailure);  // semicolon only at the very end

If a chained call seems to silently do nothing, check the line above
it for a stray semicolon first.
                    

Async / Await

async/await is SYNTACTIC SUGAR over promises — it doesn't replace promises or add new capability, it's just a way to write promise-based code that reads like synchronous code. Setup mistakes here are almost always about WHERE the async/await keywords go, so that's the focus below.

Syntax Description Example
async function name() {} async goes BEFORE function in a declaration. async function msg() { }
const x = async function() {} Async function EXPRESSION — async goes right before function, still no name needed. const msg = async function() { }
const x = async () => {} Async ARROW function — async goes before the parameter parens, not after them. const msg = async () => { }
await somePromise Pauses execution INSIDE the async function only, until the promise settles. Returns the RESOLVED value directly — no .then() needed. const value = await fetchData();
// async placement — all three function styles:
async function declared(num) { return num; }           // BEFORE `function`
const expressed = async function(num) { return num; }  // BEFORE `function`, after `=`
const arrow = async (num) => { return num; }           // BEFORE `(`, NOT after `=>`
Setup checklist — if await throws "await is only valid in async
functions" or similar, check these in order:

1. Is the ENCLOSING function actually marked async? `await` only works
   directly inside a function that has `async` on it — not just
   anywhere in the file, and not automatically inside callbacks nested
   in an async function unless THEY are also separately marked async.

2. Is `async` in the right SPOT for the function style you're using?
     async function name() { }        ← before `function`
     async () => { }                  ← before the ( ), not after
     const x = async function() { }   ← before `function`, after `=`

3. Are you missing `await` when calling something that returns a
   promise? Without it, you get the PROMISE OBJECT itself, not the
   resolved value — a very common silent bug:

     const value = fetchData();       // value is a Promise, NOT the data
     const value = await fetchData(); // value IS the resolved data

   What this looks like in practice — both are silent, no crash:
     console.log(value);              // Promise {  }
     console.log(`Hi ${value}`);      // 'Hi [object Promise]'
                                      // the promise object gets coerced
                                      // to a string inside a template literal

4. An async function ALWAYS returns a promise — even if you `return`
   a plain value inside it. That plain value gets auto-wrapped:
     async function getNum() { return 5; }
     getNum();              // returns a Promise that resolves to 5, NOT 5
     const n = await getNum();  // n is 5 — only `await` unwraps it
                    
function helloWorld() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve('Hello World!');
    }, 2000);
  });
}

async function msg() {
  const result = await helloWorld();   // pauses HERE for ~2s
  console.log('Message:', result);
}

msg();                                   // starts the async function
console.log('This logs FIRST');          // runs immediately — await only
                                          // pauses inside msg(), not the
                                          // rest of the program
Dependent promises — async/await vs chained .then():

Both do the same thing: use the result of one promise as the input to
the next. The async/await version reads top to bottom and earlier
values stay in scope naturally — no nesting or careful return needed.

// Native promise chain:
function nativeVersion() {
  returnsFirstPromise()
    .then((firstValue) => {
      console.log(firstValue);
      return returnsSecondPromise(firstValue);  // must remember to return
    })
    .then((secondValue) => {
      console.log(secondValue);
    });
}

// async/await — same result, reads like synchronous code:
async function asyncVersion() {
  let firstValue = await returnsFirstPromise();
  console.log(firstValue);
  let secondValue = await returnsSecondPromise(firstValue);
  console.log(secondValue);
}
                    
Error handling — try...catch wraps await, same mechanism as sync code:

async function getUser() {
  try {
    const response = await fetchUser();   // if this rejects...
    console.log(response);
  } catch (e) {
    console.log('Something went wrong:', e);   // ...this runs instead
  }
}

Without the try...catch, a rejected await throws inside the async
function and the promise IT returns rejects instead — if nothing
catches that, you get an unhandled rejection.

Why try...catch beats a long .catch() chain: with chaining, if any
step rejects, the single .catch() at the end fires — but you can't
tell which step caused it. With try...catch, the catch block fires
only for errors inside THAT try block, so the location is clear.

Since async functions return a promise, you can also attach .catch()
externally — sometimes used in global scope for top-level error catching:

  async function riskyOp() {
    let result = await somePromise();
    console.log(result);
  }

  riskyOp().catch(err => console.log('Top-level catch:', err));
                    
Concurrency with async/await — don't accidentally serialize independent work:

// SLOWER — each await blocks the next line, even though these three
// requests don't depend on each other at all:
async function getAllSlow() {
  const a = await fetchA();   // waits for A to finish...
  const b = await fetchB();   // ...before even STARTING B
  const c = await fetchC();   // ...then waits again before C
}

// FASTER — start all three promises first (don't await yet), THEN await:
async function getAllFast() {
  const promiseA = fetchA();   // starts immediately, not awaited yet
  const promiseB = fetchB();   // starts immediately too — runs alongside A
  const promiseC = fetchC();   // same — all three are now in flight together

  const a = await promiseA;    // now collect the results
  const b = await promiseB;
  const c = await promiseC;
}

// Or equivalently, with Promise.all() for the same concurrent effect:
async function getAllWithPromiseAll() {
  const [a, b, c] = await Promise.all([fetchA(), fetchB(), fetchC()]);
}

Rule of thumb: only await something the moment BEFORE you actually
need its value. Constructing the promise (calling the function) and
awaiting it are two separate steps — doing them on the same line for
multiple independent calls is what causes accidental serialization.

You can also await directly inside a console.log() — no need for a
separate variable just to log a result:

  console.log(await firstPromise, await secondPromise);
                    

HTTP & TCP

HTTP (Hypertext Transfer Protocol) structures requests and responses over the internet. TCP (Transmission Control Protocol) manages the actual network connection — TCP is the channel, HTTP is the language both sides speak over it.

Term Meaning
HTTP Hypertext Transfer Protocol — the command language both sides of a connection must follow to communicate.
TCP Transmission Control Protocol — manages the channel between client and server. HTTP rides on top of TCP.
Client The device making the request (e.g. your browser).
Server The device responding to the request (e.g. codecademy.com).
URL Uniform Resource Locator — the address that identifies where a resource lives on the web.
How a request works — step by step:

1. You type a URL into the browser.
2. Browser extracts the domain name and asks a DNS server to resolve
   it to an IP address.
3. Browser opens a TCP connection to that IP address.
4. Browser sends an HTTP GET request over that connection.
5. Server processes the request and sends back an HTTP response.
6. TCP connection is closed (HTTP/1.0) or reused (HTTP/1.1).
                    
GET request format:

GET / HTTP/1.1
Host: www.codecademy.com

First line: request type, path on the server, protocol version.
Second line: the server's domain name.

Successful response:

HTTP/1.1 200 OK
Content-Type: text/html
[page content follows]

Failed response:

HTTP/1.1 404 NOT FOUND
                    
HTTP/1.0 vs HTTP/1.1:

HTTP/1.0 — opens a NEW TCP connection for every resource (the HTML
page, each image, each stylesheet). Slow for pages with many assets.

HTTP/1.1 — REUSES the same connection across multiple requests.
Significantly less delay — the standard today.

HTTPS (HTTP Secure):

Encrypts data sent and received using SSL/TLS — essential when passing
sensitive information (passwords, payment details). The server must
obtain a certificate from a Certificate Authority to support HTTPS.
                    

Web APIs

An API (Application Programming Interface) is a software tool that makes it easier to access another application's functionality — a reusable solution to a repeated problem, rather than writing it yourself from scratch.

Type Description Examples
Browser APIs Built into the browser — give access to browser functionality. Some require user permission. Geolocation, Audio, Cryptography, VR — full list on MDN
Third-party APIs Provided by companies — give access to external data or functionality you wouldn't otherwise have. OpenWeather, Google Maps, Spotify, Rebrandly
Working with a third-party API:

1. Read the documentation — it specifies endpoints, required parameters,
   auth method, rate limits, and response format.
2. Sign up and generate an API key if required — a unique token that
   grants your account access. Many APIs offer free and paid tiers.
3. Pass any required parameters with your request (city name, search
   term, date range, etc.) as specified in the docs.
4. Handle the response — most APIs return JSON, which you parse and
   use in your app however you choose.
                    
API keys — keep them secret:

const apiKey = 'YOUR_API_KEY_HERE';

- Treat an API key like a password — unique to your account.
- Never share it in forums, screenshots, or code reviews.
- Never commit it to git — if pushed to a public repo, assume it
  is compromised and rotate (regenerate) it immediately.
- Safer approaches: store in an environment variable, a config file
  listed in .gitignore, or a backend proxy that holds the key
  server-side so it never reaches the client.
- For Rebrandly: key goes in the request HEADERS, not the URL or body:
    headers: { 'Content-type': 'application/json', 'apikey': apiKey }
                    

REST

REST (REpresentational State Transfer) is an architectural style for how clients and servers communicate. RESTful systems have two key characteristics: statelessness and separation of client and server.

REST architecture — where the API sits:

  Database ↔ Web Server ↔ RESTful API ↔ Your Website / Application

The RESTful API is the middle layer — it receives requests from your
app, communicates with the web server and database, and sends back
responses. Your app never touches the database directly.

Stateless: the server doesn't need to know what state the client is
in (and vice versa). Each request is fully self-contained.

Client/server separation: both sides are independent — either can be
updated without affecting the other, as long as both still follow the
same request/response format.
                    
HTTP verb Action Success code
GET Retrieve a resource or collection of resources. 200 OK
POST Create a new resource. 201 CREATED
PUT Update a specific resource (by id). 200 OK
DELETE Remove a specific resource (by id). 204 NO CONTENT
Status code Meaning
200 OK Standard success response.
201 CREATED Resource successfully created (typical POST response).
204 NO CONTENT Success — nothing returned in the response body (typical DELETE response).
400 BAD REQUEST Request cannot be processed — bad syntax, missing data, or other client error.
403 FORBIDDEN Client does not have permission to access this resource.
404 NOT FOUND Resource could not be found — deleted, not yet created, or URL wrong.
500 INTERNAL SERVER ERROR Unexpected server-side failure with no more specific information available.
Request structure — a REST request has up to four parts:

1. HTTP verb     — what operation to perform (GET, POST, PUT, DELETE)
2. Header        — metadata, including the Accept field which tells
                   the server what content types the client can handle
3. Path          — the URL path to the resource
4. Body          — optional data payload (used with POST and PUT)

MIME types — format of Accept (request) and Content-Type (response)
headers. Format is type/subtype:

  text/html     text/css        text/plain
  image/png     image/jpeg      image/gif
  audio/wav     video/mp4
  application/json              application/pdf

Path conventions:
- Use plural nouns: /customers, /orders
- Append an id for a specific resource: /customers/223
- Nest to show hierarchy: /customers/223/orders/12
- POST to a collection (server generates the id):
    POST /customers
- GET, PUT, DELETE a specific resource (id required):
    GET    /customers/223
    PUT    /customers/223
    DELETE /customers/223
                    

JSON

JSON (JavaScript Object Notation) is a language-independent standard format for storing and exchanging data. It's the de facto format most web APIs use for responses — any programming language can parse it.

// Valid JSON — all keys and strings in double quotes, no trailing commas:
{
  "student": {
    "name": "Rumaisa Mahoney",
    "age": 30,
    "fullTime": true,
    "languages": [ "JavaScript", "HTML", "CSS" ],
    "GPA": 3.9,
    "favoriteSubject": null
  }
}
Rule Detail
Keys in double quotes "name" — NOT name or 'name'
Strings in double quotes "hello" — NOT 'hello'
No trailing commas ["JS", "CSS"] — NOT ["JS", "CSS",]
No comments JSON does not support // or /* */
// Valid JavaScript — but INVALID JSON (three mistakes):
{
  name: "Rumaisa",          // unquoted key
  "languages": ["JS", ],    // trailing comma
  'subject': null           // single-quoted key
}
JSON data type Example
string "Rumaisa Mahoney" — double quotes required
number 30, 3.9
object { "key": "value" }
array [ "JavaScript", "HTML" ]
boolean true or false — no quotes
null null — no quotes
Dates are not a JSON type — store as ISO 8601 strings:

  "2014-01-01T23:28:56.782Z"

Every language has built-in tools to convert this string into a usable
date object once received.

JSON vs JavaScript object literal — the key difference:

JSON requires double quotes on ALL keys and ALL string values.
JS object literals don't require quotes on keys, and accept both
single and double quotes on string values.

  JSON:       { "name": "Kate", "age": 30 }
  JavaScript: {  name:  'Kate',  age:  30 }
                    
// JSON.parse() — JSON string → JavaScript object you can work with:
const jsObject = JSON.parse(jsonData);
jsObject.student.name;          // dot notation
jsObject['student']['name'];    // bracket notation — both work

// JSON.stringify() — JavaScript object → JSON string ready to send:
const jsonData = JSON.stringify(jsObject);

// Edit-and-re-stringify — parse, modify, convert back:
let obj = JSON.parse(jsonData);
obj.parent.age = 35;
const updated = JSON.stringify(obj);   // ready to send

Fetch API

fetch() is the browser's built-in function for making HTTP requests. It returns a promise that resolves to a Response object. Use it with .then() chaining or async/await — both patterns produce the same result.

// GET — .then() version:
fetch('https://api-to-call.com/endpoint')
  .then(response => {
    if (response.ok) { return response.json(); }  // parse body as JSON
    throw new Error('Request failed!');            // bad status → throw
  }, networkError => {
    console.log(networkError.message);             // server unreachable
  })
  .then(jsonResponse => {
    // use the data here
  });
// GET — async/await version (same result, cleaner to read):
const getData = async () => {
  try {
    const response = await fetch('https://api-to-call.com/endpoint');
    if (response.ok) {
      const jsonResponse = await response.json();
      // use the data here
    }
    throw new Error('Request failed!');
  } catch (error) {
    console.log(error);
  }
};
// POST — only the fetch() call changes; the chain/try-catch is identical:
fetch(url, {
  method: 'POST',
  headers: {
    'Content-type': 'application/json',
    'apikey': apiKey          // API key in headers — common pattern
  },
  body: JSON.stringify({ destination: urlToShorten })
})
  .then(response => {
    if (response.ok) { return response.json(); }
    throw new Error('Request failed!');
  }, networkError => console.log(networkError.message))
  .then(jsonResponse => { renderResponse(jsonResponse); });
// POST — async/await version:
const shortenUrl = async () => {
  const data = JSON.stringify({ destination: urlToShorten });
  try {
    const response = await fetch(url, {
      method: 'POST',
      headers: { 'Content-type': 'application/json', 'apikey': apiKey },
      body: data
    });
    if (response.ok) {
      const jsonResponse = await response.json();
      renderResponse(jsonResponse);
    }
  } catch (error) {
    console.log(error);
  }
};
Query strings — appending parameters to a URL:

A query string starts with ? followed by key=value pairs.
Multiple parameters are separated by &:

  const queryString = `?key=value`;
  const queryString = `?key1=value1&key2=value2`;

  // Built dynamically:
  const endpoint = `${url}?sl=${wordQuery}`;
  const endpoint = `${url}?sl=${wordQuery}&max=10`;

The ? marks the start — everything after it is the query string,
not part of the base URL path.
                    
Key point Detail
response.ok true if status is 200–299. Use this to check success without checking the exact code.
response.json() Returns a PROMISE — needs its own await (or return in .then()). A common mistake is awaiting only fetch() and treating .json() as synchronous.
Network error vs bad status The second argument to .then() (or catch) handles network failure (server unreachable). response.ok handles bad status codes (server responded, but with an error).
GET vs POST Only the options object passed to fetch() changes. The .then() chain or try...catch structure is identical for both.

Web Applications

A web application is software that runs on a remote server and is accessed through a web browser — no installation required. Where a website is defined by its content, a web app is defined by its interaction with the user (sending and consuming data).

Term Description
Static website Set of inter-related pages; defined by content. Simple page loads are fine.
Web application Application software on a web server; defined by user interaction. Requires more complex architecture.

Three-Layer Architecture

Most web apps are built in three layers. Complex apps may add more (e.g. an integration layer between application and storage to insulate from database changes).

Layer Also called Responsibility
Presentation Browser / front-end Displays the UI; relays user data to the application layer.
Application Server / back-end Processes data; contains business logic; communicates with storage.
Storage Database Persists data for later retrieval.

Single-Page Applications (SPAs)

A single-page application (SPA) is a web app that dynamically rewrites the current page with new data from the server using JavaScript — instead of loading entirely new pages on each request. The result feels like a desktop or mobile app inside the browser.

Multi-Page App (MPA) Single-Page App (SPA)
On each interaction New request → server returns full new page files JS updates only the part of the page that changed
Speed Slower — full page reload each time Faster — small data requests only
Initial load Fast — just what's needed Slower — more files upfront
SEO Easier — each page is indexable Harder — content loaded by JS may not be indexed
Browser back button Works as expected Can behave unexpectedly without extra handling

Common SPA Frameworks

Framework / Library Approach
React Component-based; components re-render based on state and user data. Most widely used.
Vue.js Templating within a single HTML file; considered more traditional and easier to learn.
Others AngularJS, Ember.js, Knockout.js, Meteor.js — same goals, different approaches.

Pros & Cons

Pros Cons
Fast, real-time feel — no full page reloads Longer initial load — more files required upfront
Components are reusable across projects SEO challenges — JS-loaded content harder to index
Easier mobile migration — back-end and front-end are decoupled Browser back/forward history may behave unexpectedly