Skip to main content

API Integration Overview

The project fetches real product data from the Platzi Fake Store API. You’ll learn how to make HTTP requests, handle responses, manage loading states, and deal with errors.

What You’ll Learn

  • Fetch API for HTTP requests
  • Async/await for asynchronous code
  • Try/catch error handling
  • Loading states and UX feedback
  • TypeScript interfaces for API responses

Understanding the API

API Endpoint

const API_URL = "https://api.escuelajs.co/api/v1/products";
This returns an array of products with details like title, price, images, and category.

Sample Response

[
  {
    "id": 1,
    "title": "Smartphone X",
    "price": 899,
    "description": "Latest smartphone with amazing features",
    "images": [
      "https://i.imgur.com/QkIa5tT.jpeg",
      "https://i.imgur.com/He6kJcI.jpeg"
    ],
    "category": {
      "id": 2,
      "name": "Electronics",
      "image": "https://i.imgur.com/ZANVnHE.jpeg",
      "slug": "electronics"
    },
    "slug": "smartphone-x"
  }
]

TypeScript Interfaces

Define interfaces to describe the API response structure:
interface Category {
  id: number;
  name: string;
  image: string;
  slug: string;
}

interface Product {
  id: number;
  title: string;
  slug: string;
  price: number;
  description: string;
  category: Category; // Nested interface
  images: string[];    // Array of strings
}
Interfaces are TypeScript’s way of defining the “shape” of data. They provide autocomplete and catch errors before runtime.

Why Use Interfaces?

Without interfaces:
// No type checking, easy to make mistakes
product.titel  // Typo! Runtime error
product.price.toFixed()  // If price is undefined, crash!
With interfaces:
const product: Product = await fetchProduct();
product.titel  // ❌ TypeScript error: Property 'titel' does not exist
product.title  // ✅ Autocomplete works!

The Fetch Function

1

Create Async Function

Define an async function that returns a Promise:
async function fetchProducts(limit: number = 20): Promise<Product[]> {
  // Function body
}
Breakdown:
  • async - This function contains asynchronous operations
  • limit: number = 20 - Parameter with default value
  • Promise<Product[]> - Returns a promise that resolves to an array of Products
2

Build the URL

Use template literals to create the request URL:
const url = `${API_URL}?limit=${limit}`;
console.log(`Fetching: ${url}`);
Example result: https://api.escuelajs.co/api/v1/products?limit=20
3

Make the Request

Use fetch() with await:
const response = await fetch(url);
What happens here:
  1. Browser sends HTTP GET request to the URL
  2. await pauses execution until response arrives
  3. response contains status, headers, and body
4

Check Response Status

Verify the request was successful:
if (!response.ok) {
  throw new Error(`Error HTTP: ${response.status}`);
}
HTTP Status Codes:
  • 200-299 - Success (response.ok is true)
  • 404 - Not found
  • 500 - Server error
5

Parse JSON

Extract and parse the JSON data:
const data = await response.json() as Product[];
console.log(`Productos recibidos: ${data.length}`);
return data;
as Product[] tells TypeScript the JSON matches our Product interface.

Complete Fetch Function

async function fetchProducts(limit: number = 20): Promise<Product[]> {
  // Build URL with query parameter
  const url = `${API_URL}?limit=${limit}`;
  console.log(`Fetching: ${url}`);
  
  // Make HTTP request
  const response = await fetch(url);
  
  // Check if successful
  if (!response.ok) {
    throw new Error(`Error HTTP: ${response.status}`);
  }
  
  // Parse and return JSON
  const data = await response.json() as Product[];
  console.log(`Productos recibidos: ${data.length}`);
  
  return data;
}

Understanding Async/Await

The Problem: Callback Hell

Before async/await, we used callbacks:
// Old way - callback hell
fetch(url)
  .then(response => response.json())
  .then(data => {
    // Use data
  })
  .catch(error => {
    // Handle error
  });

The Solution: Async/Await

// Modern way - clean and readable
try {
  const response = await fetch(url);
  const data = await response.json();
  // Use data
} catch (error) {
  // Handle error
}
async/await makes asynchronous code look synchronous, making it much easier to read and maintain.

Promise States

A Promise can be in three states:
  1. Pending - Operation in progress
  2. Fulfilled - Operation succeeded (resolved)
  3. Rejected - Operation failed (error)
fetch(url)

Pending... (waiting for server)

  Success?  ----- Yes -----> Fulfilled (data received)

   └----- No -----> Rejected (error)

Loading Products Function

This function orchestrates the entire loading process:
async function loadProducts(): Promise<void> {
  // 1. Set loading state
  appState.status = LoadingState.Loading;
  appState.error = null;
  updateUI(); // Show spinner
  
  try {
    // 2. Fetch products (might fail)
    const products = await fetchProducts(20);
    
    // 3. Success - update state
    appState.status = LoadingState.Success;
    appState.products = products;
    
  } catch (error) {
    // 4. Error - capture and display
    appState.error = error instanceof Error 
      ? error.message 
      : "Error desconocido";
    appState.status = LoadingState.Error;
    
    console.error("Error al cargar productos:", error);
  }
  
  // 5. Update UI (success or error)
  updateUI();
}

State Flow Diagram

User clicks "Cargar Productos"

appState.status = Loading

updateUI() → Show spinner

fetchProducts()

    ├─── Success → status = Success
    │              products = data
    │              updateUI() → Render products

    └─── Error → status = Error
                 error = message
                 updateUI() → Show error

Error Handling

Try/Catch Block

try {
  // Code that might throw an error
  const data = await riskyOperation();
} catch (error) {
  // Code that runs if error occurs
  console.error("Something went wrong:", error);
}

Type-Safe Error Handling

catch (error) {
  // error is type 'unknown' in TypeScript
  
  // Check if it's an Error object
  if (error instanceof Error) {
    appState.error = error.message; // Has .message property
  } else {
    appState.error = "Error desconocido";
  }
}
TypeScript doesn’t know what type error is in a catch block. Use instanceof Error to safely access the .message property.

Common Fetch Errors

Error TypeCauseSolution
Network ErrorNo internet connectionShow “Check your connection”
404 Not FoundWrong URLVerify API endpoint
500 Server ErrorAPI is downShow “Try again later”
CORS ErrorCross-origin blockedAPI must allow your domain
TimeoutRequest too slowAdd timeout handling
Parse ErrorInvalid JSONValidate response format

Loading States with Enum

We use an enum to define all possible states:
enum LoadingState {
  Idle = "IDLE",       // Initial state
  Loading = "LOADING", // Fetching data
  Success = "SUCCESS", // Data loaded
  Error = "ERROR"      // Failed to load
}

Why Enum vs Strings?

Without enum (error-prone):
if (status === "loadng") { } // Typo! No error
if (status === "LOADING") { } // Wrong case! No error
With enum (safe):
if (status === LoadingState.Loading) { } // ✅ Correct
if (status === LoadingState.Loadng) { }  // ❌ Compile error

UI Updates Based on State

function updateUI(): void {
  const grid = getElement<HTMLDivElement>("#products-grid");
  const loading = getElement<HTMLDivElement>("#products-loading");
  const error = getElement<HTMLDivElement>("#products-error");
  const loadBtn = getElement<HTMLButtonElement>("#load-products-btn");
  
  // Hide all state indicators by default
  loading.hidden = true;
  error.hidden = true;
  
  // Show appropriate state
  switch (appState.status) {
    case LoadingState.Idle:
      grid.innerHTML = `<p class="products__empty-state">Haz clic en "Cargar Productos"</p>`;
      loadBtn.disabled = false;
      loadBtn.textContent = "Cargar Productos";
      break;
      
    case LoadingState.Loading:
      grid.innerHTML = '';
      loading.hidden = false; // Show spinner
      loadBtn.disabled = true;
      loadBtn.textContent = "Cargando...";
      break;
      
    case LoadingState.Success:
      renderProducts(); // Display products
      loadBtn.disabled = false;
      loadBtn.textContent = "Recargar Productos";
      break;
      
    case LoadingState.Error:
      grid.innerHTML = '';
      error.hidden = false; // Show error message
      loadBtn.disabled = false;
      loadBtn.textContent = "Reintentar";
      break;
  }
}

Request Optimization

Limiting Results

fetchProducts(20) // Only fetch 20 products
Why limit?
  • Faster loading
  • Less bandwidth
  • Better UX (pagination later)

Caching Strategy

let cachedProducts: Product[] | null = null;

async function loadProducts(): Promise<void> {
  // Use cache if available
  if (cachedProducts) {
    appState.products = cachedProducts;
    appState.status = LoadingState.Success;
    updateUI();
    return;
  }
  
  // Otherwise fetch
  appState.status = LoadingState.Loading;
  updateUI();
  
  try {
    const products = await fetchProducts(20);
    cachedProducts = products; // Cache for next time
    appState.products = products;
    appState.status = LoadingState.Success;
  } catch (error) {
    // Error handling...
  }
  
  updateUI();
}

Request Timeout

function fetchWithTimeout(url: string, timeout: number = 5000): Promise<Response> {
  return Promise.race([
    fetch(url),
    new Promise<Response>((_, reject) => 
      setTimeout(() => reject(new Error('Request timeout')), timeout)
    )
  ]);
}

// Use it
const response = await fetchWithTimeout(API_URL, 5000);

Complete Code Reference

API URL: /workspace/source/mi-tutorial/src/main.ts:60 Interfaces: /workspace/source/mi-tutorial/src/main.ts:109-126 Fetch Function: /workspace/source/mi-tutorial/src/main.ts:409-431 Load Function: /workspace/source/mi-tutorial/src/main.ts:931-960 Update UI: /workspace/source/mi-tutorial/src/main.ts:536-589

Testing API Calls

Open browser console and try:
// Test basic fetch
fetch('https://api.escuelajs.co/api/v1/products?limit=5')
  .then(r => r.json())
  .then(data => console.log(data));

// Test with async/await
async function test() {
  const response = await fetch('https://api.escuelajs.co/api/v1/products?limit=5');
  const data = await response.json();
  console.log(data);
}
test();

Next Steps

State Management

Learn how application state is managed centrally

Product Catalog

See how fetched products are displayed

Build docs developers (and LLMs) love