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()
}
// 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)
}
// 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')
Finished reading?Mark it done to track your progress.
Tip: use the ← and → keys to move between lessons.