Working with APIs

Connect your app to the world. CRUD, auth, pagination, and real patterns.

REST Basics

RESTful APIs use HTTP methods on URL endpoints.

MethodPurposeExample
GETReadGET /api/users
POSTCreatePOST /api/users
PUTReplacePUT /api/users/1
PATCHUpdatePATCH /api/users/1
DELETEDeleteDELETE /api/users/1
// REST URL patterns:
// Collection:  /api/users        (list of users)
// Resource:    /api/users/123    (single user)
// Nested:      /api/users/123/posts (user's posts)
// Filtered:    /api/users?role=admin&limit=10

// A REST API response typically looks like:
{
  "data": [...],
  "meta": { "total": 100, "page": 1 }
}

CRUD Operations

Create, Read, Update, Delete — the four fundamental operations.

const API_URL = "https://api.example.com";

// CREATE — add a new resource:
async function createUser(userData) {
  const res = await fetch(`${API_URL}/users`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(userData),
  });
  if (!res.ok) throw new Error(`Create failed: ${res.status}`);
  return res.json(); // { id: 4, name: "Alice", ... }
}

// READ — get resources:
async function getUsers() {
  const res = await fetch(`${API_URL}/users`);
  if (!res.ok) throw new Error(`Fetch failed: ${res.status}`);
  return res.json();
}

async function getUser(id) {
  const res = await fetch(`${API_URL}/users/${id}`);
  if (!res.ok) throw new Error(`User not found`);
  return res.json();
}
// UPDATE — modify existing resource:
async function updateUser(id, updates) {
  const res = await fetch(`${API_URL}/users/${id}`, {
    method: "PATCH",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(updates),
  });
  if (!res.ok) throw new Error(`Update failed: ${res.status}`);
  return res.json();
}

// DELETE — remove a resource:
async function deleteUser(id) {
  const res = await fetch(`${API_URL}/users/${id}`, {
    method: "DELETE",
  });
  if (!res.ok) throw new Error(`Delete failed: ${res.status}`);
  return res.status === 204 ? null : res.json();
}

Authentication

Prove who you are to the API.

// Method 1: Bearer Token (most common)
async function fetchProtected(url, token) {
  const res = await fetch(url, {
    headers: {
      "Authorization": `Bearer ${token}`,
    },
  });
  
  if (res.status === 401) {
    // Token expired — refresh it
    const newToken = await refreshToken();
    return fetchProtected(url, newToken);
  }
  
  return res.json();
}

// Method 2: API Key (simpler)
const res = await fetch(`${API_URL}/data?api_key=${API_KEY}`);
// or in headers:
const res2 = await fetch(url, {
  headers: { "X-API-Key": API_KEY },
});
// Login flow:
async function login(email, password) {
  const res = await fetch("/api/auth/login", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ email, password }),
  });
  
  if (!res.ok) throw new Error("Invalid credentials");
  
  const { token, refreshToken } = await res.json();
  
  // Store securely (httpOnly cookie is best)
  localStorage.setItem("token", token);
  return token;
}

Pagination

Handle large datasets by loading in pages.

// Offset-based pagination:
async function getUsers(page = 1, limit = 20) {
  const res = await fetch(
    `/api/users?page=${page}&limit=${limit}`
  );
  const data = await res.json();
  // { users: [...], total: 150, page: 1, pages: 8 }
  return data;
}

// Load all pages:
async function getAllUsers() {
  const allUsers = [];
  let page = 1;
  let hasMore = true;
  
  while (hasMore) {
    const { users, pages } = await getUsers(page);
    allUsers.push(...users);
    hasMore = page < pages;
    page++;
  }
  
  return allUsers;
}
// Cursor-based pagination (better for real-time data):
async function fetchFeed(cursor = null) {
  const url = cursor
    ? `/api/feed?after=${cursor}&limit=20`
    : "/api/feed?limit=20";
    
  const res = await fetch(url);
  const { items, nextCursor } = await res.json();
  
  return { items, nextCursor };
  // nextCursor = null means no more items
}

// Infinite scroll:
let cursor = null;
async function loadMore() {
  const { items, nextCursor } = await fetchFeed(cursor);
  cursor = nextCursor;
  appendToList(items);
  if (!nextCursor) hideLoadMoreButton();
}

Building an API Client

Wrap fetch in a reusable utility.

// A simple, reusable API client:
class ApiClient {
  constructor(baseUrl, token = null) {
    this.baseUrl = baseUrl;
    this.token = token;
  }

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

    const res = await fetch(url, { ...options, headers });

    if (!res.ok) {
      const error = await res.json().catch(() => ({}));
      throw new Error(error.message || `HTTP ${res.status}`);
    }

    if (res.status === 204) return null;
    return res.json();
  }

  get(endpoint) {
    return this.request(endpoint);
  }

  post(endpoint, data) {
    return this.request(endpoint, {
      method: "POST",
      body: JSON.stringify(data),
    });
  }

  patch(endpoint, data) {
    return this.request(endpoint, {
      method: "PATCH",
      body: JSON.stringify(data),
    });
  }

  delete(endpoint) {
    return this.request(endpoint, { method: "DELETE" });
  }
}
// Usage:
const api = new ApiClient("https://api.example.com", token);

const users = await api.get("/users");
const newUser = await api.post("/users", { name: "Alice" });
await api.patch(`/users/${newUser.id}`, { name: "Bob" });
await api.delete(`/users/${newUser.id}`);

FAQ

Common questions about working with APIs.