FullStackJS Camp
Módulo 4·practica·6h
Objetivos de aprendizaje
  • Entender qué es una API REST y el protocolo HTTP.
  • Usar fetch() para peticiones GET, POST, PUT y DELETE.
  • Manejar headers, Content-Type y autenticación básica.
  • Distinguir errores de red de errores HTTP (status codes).
  • Consumir una API pública real (OMDb) con async/await.

Consumo de APIs con Fetch

Una API REST expone datos y operaciones a través de URLs (endpoints) usando el protocolo HTTP. fetch() es la forma nativa de comunicarse con ellas desde JavaScript.


Repaso rápido de HTTP

MétodoUsoBody
GETLeer datosNo
POSTCrear recurso
PUTReemplazar recurso
PATCHActualizar parcialmente
DELETEEliminar recursoNo
StatusSignificado
200 OKÉxito
201 CreatedRecurso creado
400 Bad RequestError del cliente
401 UnauthorizedSin autenticación
404 Not FoundNo existe
500 Internal Server ErrorError del servidor

fetch() básico — GET

javascript
// fetch retorna una Promise<Response>
// .json() también retorna una Promise<datos>

async function obtenerUsuario(id) {
  const response = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`);

  // ⚠️ fetch NO lanza error en 404 o 500 — solo en fallos de red
  if (!response.ok) {
    throw new Error(`HTTP ${response.status}: ${response.statusText}`);
  }

  const usuario = await response.json();
  return usuario;
}

try {
  const user = await obtenerUsuario(1);
  console.log(user.name);    // "Leanne Graham"
  console.log(user.email);   // "Sincere@april.biz"
} catch (error) {
  console.error("Error:", error.message);
}

POST — Crear un recurso

javascript
async function crearPost(titulo, cuerpo, userId) {
  const response = await fetch("https://jsonplaceholder.typicode.com/posts", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      // Si la API requiere autenticación:
      // "Authorization": `Bearer ${token}`,
    },
    body: JSON.stringify({
      title: titulo,
      body: cuerpo,
      userId,
    }),
  });

  if (!response.ok) {
    const errorData = await response.json().catch(() => ({}));
    throw new Error(`Error ${response.status}: ${errorData.message ?? response.statusText}`);
  }

  return response.json(); // { id: 101, title: ..., body: ..., userId: ... }
}

const nuevoPost = await crearPost("Mi primer post", "Contenido del post", 1);
console.log(nuevoPost.id); // 101

PUT y DELETE

javascript
// Actualizar completamente
async function actualizarPost(id, datos) {
  const res = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`, {
    method: "PUT",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(datos),
  });
  if (!res.ok) throw new Error(`Error ${res.status}`);
  return res.json();
}

// Eliminar
async function eliminarPost(id) {
  const res = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`, {
    method: "DELETE",
  });
  if (!res.ok) throw new Error(`Error ${res.status}`);
  return true; // 200 con body vacío {}
}

Manejo de errores robusto

Crea un wrapper que centralice la lógica de error:

javascript
class ApiError extends Error {
  constructor(status, message) {
    super(message);
    this.name  = "ApiError";
    this.status = status;
  }
}

async function apiFetch(url, opciones = {}) {
  let response;

  try {
    response = await fetch(url, opciones);
  } catch {
    throw new ApiError(0, "Sin conexión a internet");
  }

  if (!response.ok) {
    let mensaje = `HTTP ${response.status}`;
    try {
      const body = await response.json();
      mensaje = body.message ?? body.error ?? mensaje;
    } catch { /* body no es JSON */ }
    throw new ApiError(response.status, mensaje);
  }

  return response.json();
}

// Uso
try {
  const datos = await apiFetch("https://api.ejemplo.com/productos");
  console.log(datos);
} catch (error) {
  if (error instanceof ApiError) {
    if (error.status === 401) redirigirALogin();
    else mostrarError(error.message);
  } else {
    throw error;
  }
}

Proyecto: Buscador de Películas (OMDb API)

La OMDb API permite buscar películas y series. Requiere una API Key gratuita (registro en omdbapi.com).

Parámetros principales

ParámetroUso
s=textoBuscar por título (lista)
i=tt0372784Buscar por IMDb ID (detalle)
t=BatmanBuscar por título exacto
type=movie|seriesFiltrar por tipo
y=2008Filtrar por año
page=2Paginación (búsqueda por s)

Implementación

javascript
const API_KEY = "TU_API_KEY"; // Registro gratuito en omdbapi.com
const BASE_URL = "https://www.omdbapi.com";

async function buscarPeliculas(query, pagina = 1) {
  const url = new URL(BASE_URL);
  url.searchParams.set("apikey", API_KEY);
  url.searchParams.set("s", query);
  url.searchParams.set("type", "movie");
  url.searchParams.set("page", pagina);

  const data = await apiFetch(url.toString());

  if (data.Response === "False") {
    throw new Error(data.Error ?? "Sin resultados");
  }

  return {
    peliculas: data.Search,
    total: Number(data.totalResults),
    paginas: Math.ceil(Number(data.totalResults) / 10),
  };
}

async function obtenerDetalle(imdbId) {
  const url = new URL(BASE_URL);
  url.searchParams.set("apikey", API_KEY);
  url.searchParams.set("i", imdbId);
  url.searchParams.set("plot", "full");

  const data = await apiFetch(url.toString());
  if (data.Response === "False") throw new Error(data.Error);
  return data;
}

// Renderizar resultados en el DOM
function renderizarPeliculas(peliculas) {
  const grid = document.querySelector("#grid-peliculas");
  grid.innerHTML = "";

  peliculas.forEach(p => {
    const article = document.createElement("article");
    article.className = "pelicula-card";
    article.dataset.imdbid = p.imdbID;

    const img = document.createElement("img");
    img.src = p.Poster !== "N/A" ? p.Poster : "/placeholder.png";
    img.alt = `Poster de ${p.Title}`;    // accesible

    const h3 = document.createElement("h3");
    h3.textContent = p.Title;

    const span = document.createElement("span");
    span.textContent = p.Year;

    article.append(img, h3, span);
    grid.appendChild(article);
  });
}

// Evento de búsqueda
document.querySelector("#form-busqueda").addEventListener("submit", async e => {
  e.preventDefault();
  const query  = e.target.querySelector("input").value.trim();
  const status = document.querySelector("#status");

  if (!query) return;

  status.textContent = "Buscando…";
  document.querySelector("#grid-peliculas").innerHTML = "";

  try {
    const { peliculas, total } = await buscarPeliculas(query);
    renderizarPeliculas(peliculas);
    status.textContent = `${total} resultados para "${query}"`;
  } catch (error) {
    status.textContent = `Error: ${error.message}`;
  }
});

CORS — Cross-Origin Resource Sharing

El navegador bloquea peticiones a dominios distintos del origen de la página salvo que el servidor envíe los headers correctos:

code
Access-Control-Allow-Origin: https://mi-app.com

Soluciones comunes:

  • La API lo soporta (OMDb, GitHub API) → funciona directo
  • La API no lo soporta → crear un proxy en tu propio servidor (Node.js) que haga la petición
javascript
// proxy en Express (Node.js)
app.get("/api/peliculas", async (req, res) => {
  const { q } = req.query;
  const data = await fetch(`https://omdbapi.com/?s=${q}&apikey=${process.env.OMDB_KEY}`);
  res.json(await data.json());
});

Práctica

Práctica interactiva · JavaScript