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
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
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
Make the Request
Use fetch() with await: const response = await fetch ( url );
What happens here:
Browser sends HTTP GET request to the URL
await pauses execution until response arrives
response contains status, headers, and body
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
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:
Pending - Operation in progress
Fulfilled - Operation succeeded (resolved)
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 Type Cause Solution Network Error No internet connection Show “Check your connection” 404 Not Found Wrong URL Verify API endpoint 500 Server Error API is down Show “Try again later” CORS Error Cross-origin blocked API must allow your domain Timeout Request too slow Add timeout handling Parse Error Invalid JSON Validate 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