Frontend Master

JavaScript Curriculum

Talking to Servers
+60 XP

Talking to Servers

hard
~28 min·60 XP

The Nexus dashboard shows a live user profile loaded from a REST API. When the page opens, JavaScript fires a fetch request, waits for the response, checks the status, parses the JSON, and renders the data — all without a page reload. When the component unmounts, the request is cancelled. When the server returns a 404, the user sees a clear message. This is the complete pattern. Everything in Chapter 01 leads here.

The anatomy of an HTTP request

Every time your JavaScript code fetches data from a server, it sends an HTTP request — a structured message with four parts:

js
Method URL Headers Body GET /api/users/42 Authorization: (none) POST /api/users Content-Type: { name: "..." } PATCH /api/users/42 Authorization: { xp: 3500 } DELETE /api/users/42 Authorization: (none)

The method tells the server what you want to do. GET retrieves data. POST creates. PATCH updates partially. DELETE removes. These are conventions — REST conventions — that nearly every API follows.

The server replies with a response: a status code, headers, and a body.


fetch() — four stages

fetch() is the browser's built-in HTTP function. Watch exactly what happens at each stage, and how the outcome changes across three different server responses:

fetch lifecycle — step through every stage
📦stage 1Build request
📡stage 2Send → wait
📬stage 3Response headers
🔍stage 4Parse body

The most important thing to remember: fetch() only rejects on network failure. A 404 or 500 response resolves the Promise normally — with response.ok = false. You must check it manually.


The Response object

When fetch() resolves, you get a Response object. It contains everything the server sent back. Click each property to learn what it holds and how to read it:

Response object — click any property to inspect it

const response = await fetch(url)

Click any property to see what it contains and how to use it.

The critical properties in every fetch call are response.ok and response.json():

js
const response = await fetch('/api/users/42') // ALWAYS check this before reading the body if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`) } // response.json() is also async — it streams and parses the body const user = await response.json()
⚠️You can only read the body once
response.json(), response.text(), and response.blob() each consume the body stream. Calling more than one on the same response throws. If you need to inspect the raw text AND parse it, read response.text() first, then JSON.parse() it yourself.

Building the pattern — four levels

The same API call, written four ways — from dangerously naive to production-ready:

incomplete

The simplest possible fetch. Works — until something goes wrong. No error checking, no loading state, no cleanup.

const response = await fetch('/api/users/42')
const user     = await response.json()
console.log(user)
// If fetch fails → unhandled rejection
// If status 404  → user = { error: "Not found" } silently

✗ Crashes silently on network errors, treats 404 as success, no way to cancel.

The production version introduces two things beyond error handling:

AbortController — lets you cancel an in-flight request. Essential in React: if a component fetches data and unmounts before the response arrives, the callback would try to update state on an unmounted component. Abort it in the cleanup function of useEffect.

Structured error objects — instead of throwing a plain string, attach status and code to the Error object so callers can handle different error types differently.


Sending data — POST and PATCH

Reading data is only half the picture. Sending data requires a body:

js
async function createUser(userData) { const response = await fetch('/api/users', { method: 'POST', headers: { 'Content-Type': 'application/json', // tells server what format 'Authorization': `Bearer ${getToken()}`, }, body: JSON.stringify(userData), // JS object → JSON string }) if (!response.ok) { const err = await response.json().catch(() => ({})) throw new Error(err.message ?? `HTTP ${response.status}`) } return response.json() } const newUser = await createUser({ name: 'Jordan', role: 'viewer' })

Key points: Content-Type: application/json tells the server how to parse the body. JSON.stringify() converts your object to a string — the body must be a string, not an object.


Query parameters and URL construction

Real APIs use query parameters for filtering, pagination, and search:

js
// Manual string building — fragile const url = `/api/users?page=${page}&limit=${limit}&search=${query}` // URLSearchParams — correct encoding, handles special characters const params = new URLSearchParams({ page: page, limit: limit, search: query, // automatically encodes "Alex Chen" → "Alex+Chen" }) const url = `/api/users?${params}` // Or with a base URL const url = new URL('/api/users', 'https://api.nexus.app') url.searchParams.set('page', page) url.searchParams.set('limit', limit) fetch(url.toString())

A complete data layer

In a real codebase, fetch calls are grouped into a data layer — functions that each own one API operation and handle all the plumbing internally:

js
// api/users.js const BASE = 'https://api.nexus.app' const headers = () => ({ 'Content-Type': 'application/json', 'Authorization': `Bearer ${getToken()}`, }) export const usersApi = { async getById(id, signal) { const res = await fetch(`${BASE}/users/${id}`, { headers: headers(), signal }) if (!res.ok) throw new Error(`getById failed: ${res.status}`) return res.json() }, async update(id, data) { const res = await fetch(`${BASE}/users/${id}`, { method: 'PATCH', headers: headers(), body: JSON.stringify(data), }) if (!res.ok) throw new Error(`update failed: ${res.status}`) return res.json() }, async delete(id) { const res = await fetch(`${BASE}/users/${id}`, { method: 'DELETE', headers: headers() }) if (!res.ok) throw new Error(`delete failed: ${res.status}`) return res.status === 204 ? null : res.json() }, }

Every component that needs user data calls usersApi.getById(id). The fetch mechanics are invisible to the component. When the API changes, you update one file.


Your challenge

loadProfile is a focused version of the pattern: fetch, check, parse, transform, return — or catch, log, return null. The ?? 0 on xp is optional chaining's partner: if the server doesn't include xp, default it to 0 rather than letting undefined silently propagate into the UI.

This is lesson-01 through lesson-13 applied. A real function, doing real work, with every edge case handled.

Challenge

Write an async function called loadProfile(userId) that fetches from '/api/users/' + userId. Check response.ok — if false, throw a new Error with 'User not found: ' + response.status. Parse the JSON body and return an object { name: data.name, role: data.role, xp: data.xp ?? 0 }. Wrap everything in try/catch — on error log 'loadProfile failed: ' + err.message and return null.

fetchHTTPRESTResponseasync-awaitAbortControllerJSONerror-handlingoptional-chaining
Talking to Servers | Nexus Learn