Lesson 14 of 18

Fetch & Working with APIs

You will learn how to send HTTP requests with fetch and handle the correctly, including the fact that a 404 does not throw an . Most apps need to talk to a server.

// GET — the shape you will write a thousand times
async function getUser(id) {
  const res = await fetch(`https://api.example.com/users/${id}`)

  // fetch only rejects on NETWORK failure. A 404 or 500 still "succeeds".
  if (!res.ok) {
    throw new Error(`HTTP ${res.status} ${res.statusText}`)
  }

  return res.json()          // res.json() is itself async
}

// POST with a JSON body
async function createUser(data) {
  const res = await fetch('https://api.example.com/users', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${token}`,
    },
    body: JSON.stringify(data),   // body must be a string, not an object
  })
  if (!res.ok) throw new Error(await res.text())
  return res.json()
}
 
JavaScript sandboxCtrl+Enter runs
// Parallel requests — do not await in sequence when they are independent
const [user, posts, comments] = await Promise.all([
  fetch('/api/user').then(r => r.json()),
  fetch('/api/posts').then(r => r.json()),
  fetch('/api/comments').then(r => r.json()),
])
// 3 requests, one round trip of waiting instead of three

// Promise.allSettled — when partial failure is acceptable
const results = await Promise.allSettled([
  fetch('/api/a'),
  fetch('/api/b'),
])
for (const r of results) {
  if (r.status === 'fulfilled') console.log('ok', r.value.url)
  else console.warn('failed', r.reason)
}

// Cancellation and timeouts with AbortController
const controller = new AbortController()
const timer = setTimeout(() => controller.abort(), 5000)

try {
  const res = await fetch('/api/slow', { signal: controller.signal })
  console.log(await res.json())
} catch (err) {
  if (err.name === 'AbortError') console.log('request timed out')
  else throw err
} finally {
  clearTimeout(timer)
}
 
JavaScript sandboxCtrl+Enter runs
// A reusable client: one place for base URL, auth, errors and retries
class ApiClient {
  constructor(baseUrl, { token, retries = 2 } = {}) {
    this.baseUrl = baseUrl
    this.token = token
    this.retries = retries
  }

  async request(path, options = {}) {
    const url = this.baseUrl + path
    const headers = {
      'Content-Type': 'application/json',
      ...(this.token && { Authorization: `Bearer ${this.token}` }),
      ...options.headers,
    }

    for (let attempt = 0; attempt <= this.retries; attempt++) {
      const res = await fetch(url, { ...options, headers })

      // Retry only on transient server errors, never on 4xx
      if (res.status >= 500 && attempt < this.retries) {
        await new Promise(r => setTimeout(r, 2 ** attempt * 200))
        continue
      }
      if (!res.ok) {
        const body = await res.text()
        throw new Error(`${res.status} ${url}: ${body}`)
      }
      return res.status === 204 ? null : res.json()
    }
  }

  get(path)        { return this.request(path) }
  post(path, data) { return this.request(path, { method: 'POST', body: JSON.stringify(data) }) }
  del(path)        { return this.request(path, { method: 'DELETE' }) }
}

const api = new ApiClient('https://api.example.com', { token: 'abc123' })
const todos = await api.get('/todos')
 
JavaScript sandboxCtrl+Enter runs
Finished reading?Mark it done to track your progress.

Tip: use the ← and → keys to move between lessons.