Cómo hacer fetch de una API en JavaScript desde cero

D
DanisCh
• 12 min de lectura
Cómo hacer fetch de una API en JavaScript desde cero
JavaScript

Una de las habilidades más importantes que necesitas como desarrollador web es saber cómo comunicarte con servicios externos para obtener o enviar datos. Esa comunicación ocurre a través de APIs, y en JavaScript la forma moderna de hacerlo es con la función fetch().

En este artículo aprenderás qué es fetch, cómo funciona, cómo manejar los diferentes tipos de peticiones HTTP y cómo tratar los errores correctamente, todo con ejemplos reales que puedes probar desde el navegador ahora mismo.

Qué es una API y por qué necesitas fetch

Una API (Application Programming Interface) es un servicio que expone datos o funcionalidades a través de internet. Cuando una aplicación necesita datos externos, como el clima de una ciudad, información de usuarios o los precios de productos, hace una petición a una API y recibe una respuesta, generalmente en formato JSON.

Antes de que existiera fetch(), los desarrolladores usaban XMLHttpRequest, una API más antigua y verbosa que requería mucho más código para hacer lo mismo. Fetch llegó con ES6 y simplificó enormemente el proceso.

// Así se hacía antes con XMLHttpRequest
const xhr = new XMLHttpRequest();
xhr.open("GET", "https://api.ejemplo.com/datos");
xhr.onload = function () {
  if (xhr.status === 200) {
    console.log(JSON.parse(xhr.responseText));
  }
};
xhr.send();

// Así se hace ahora con fetch
fetch("https://api.ejemplo.com/datos")
  .then((respuesta) => respuesta.json())
  .then((datos) => console.log(datos));

La diferencia en claridad y brevedad es evidente. Y con async/await, el código es aún más limpio.

Cómo funciona fetch por dentro

fetch() es una función global disponible en el navegador que devuelve una Promesa. Esa Promesa se resuelve con un objeto Response que representa la respuesta del servidor.

Hay un detalle muy importante que confunde a muchos principiantes: fetch solo rechaza la Promesa si hay un error de red, como que no haya conexión a internet o el servidor no sea alcanzable. Si el servidor responde con un código de error HTTP como 404 (no encontrado) o 500 (error del servidor), fetch considera eso como una respuesta exitosa y no lanza ningún error automáticamente.

Por eso siempre debes verificar el estado de la respuesta manualmente.

Tu primera petición GET con fetch

Vamos a usar JSONPlaceholder, una API pública y gratuita diseñada exactamente para practicar. No necesitas registrarte ni obtener ninguna clave.

fetch("https://jsonplaceholder.typicode.com/posts/1")
  .then((respuesta) => respuesta.json())
  .then((datos) => {
    console.log(datos);
  })
  .catch((error) => {
    console.error("Error de red:", error);
  });

Lo que ocurre paso a paso:

  • fetch(url) inicia la petición y devuelve una Promesa con el objeto Response.
  • respuesta.json() lee el cuerpo de la respuesta y lo convierte de JSON a un objeto JavaScript. Este método también devuelve una Promesa.
  • El segundo .then() recibe el objeto ya convertido y lo procesa.
  • .catch() captura cualquier error de red.

La misma petición con async/await

Usando async/await, el código es más legible y fácil de mantener. Esta es la forma que encontrarás en la mayoría de los proyectos modernos:

async function obtenerPost(id) {
  try {
    const respuesta = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`);

    if (!respuesta.ok) {
      throw new Error(`Error HTTP: ${respuesta.status}`);
    }

    const post = await respuesta.json();
    console.log(post);
    return post;

  } catch (error) {
    console.error("Algo salió mal:", error.message);
  }
}

obtenerPost(1);

Nota el uso de respuesta.ok: es una propiedad booleana que es true cuando el código de estado HTTP está entre 200 y 299. Así detectamos errores del servidor aunque fetch no los lance automáticamente.

Explorar la respuesta de fetch

El objeto Response que devuelve fetch tiene varias propiedades y métodos útiles que conviene conocer:

async function explorarRespuesta() {
  const respuesta = await fetch("https://jsonplaceholder.typicode.com/posts/1");

  console.log(respuesta.status);     // 200
  console.log(respuesta.statusText); // "OK"
  console.log(respuesta.ok);         // true
  console.log(respuesta.url);        // URL final (puede cambiar si hubo redirecciones)
  console.log(respuesta.headers.get("content-type")); // "application/json; charset=utf-8"
}

Para leer el cuerpo de la respuesta, tienes distintos métodos según el formato que esperas:

  • respuesta.json(): convierte el cuerpo de JSON a objeto JavaScript. El más usado.
  • respuesta.text(): devuelve el cuerpo como texto plano.
  • respuesta.blob(): devuelve el cuerpo como un Blob, útil para imágenes o archivos.
  • respuesta.arrayBuffer(): devuelve los datos en formato binario.
  • respuesta.formData(): devuelve los datos como FormData.

Importante: el cuerpo de la respuesta solo puede leerse una vez. Si llamas a respuesta.json() y luego a respuesta.text() en la misma respuesta, el segundo lanzará un error.

Petición GET: obtener una lista de recursos

async function obtenerTodosLosPosts() {
  try {
    const respuesta = await fetch("https://jsonplaceholder.typicode.com/posts");

    if (!respuesta.ok) {
      throw new Error(`Error: ${respuesta.status}`);
    }

    const posts = await respuesta.json();
    console.log(`Se obtuvieron ${posts.length} posts`);
    console.log("Primer post:", posts[0].title);
    return posts;

  } catch (error) {
    console.error("Error al obtener posts:", error.message);
  }
}

obtenerTodosLosPosts();

Petición POST: enviar datos al servidor

Para enviar datos, necesitas configurar el segundo argumento de fetch: el objeto de opciones. Aquí defines el método HTTP, las cabeceras y el cuerpo de la petición.

async function crearPost(titulo, cuerpo, usuarioId) {
  try {
    const respuesta = await fetch("https://jsonplaceholder.typicode.com/posts", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        title: titulo,
        body: cuerpo,
        userId: usuarioId,
      }),
    });

    if (!respuesta.ok) {
      throw new Error(`Error: ${respuesta.status}`);
    }

    const postCreado = await respuesta.json();
    console.log("Post creado:", postCreado);
    console.log("ID asignado:", postCreado.id);
    return postCreado;

  } catch (error) {
    console.error("Error al crear el post:", error.message);
  }
}

crearPost("Mi primer post", "Este es el contenido del post.", 1);

Tres puntos clave en una petición POST:

  • method: "POST" indica el método HTTP que se usará.
  • "Content-Type": "application/json" le dice al servidor que el cuerpo de la petición está en formato JSON.
  • body: JSON.stringify(objeto) convierte el objeto JavaScript a una cadena JSON para enviarlo.

Petición PUT: actualizar un recurso completo

PUT reemplaza un recurso existente con los nuevos datos proporcionados.

async function actualizarPost(id, nuevosDatos) {
  try {
    const respuesta = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`, {
      method: "PUT",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify(nuevosDatos),
    });

    if (!respuesta.ok) {
      throw new Error(`Error: ${respuesta.status}`);
    }

    const postActualizado = await respuesta.json();
    console.log("Post actualizado:", postActualizado);
    return postActualizado;

  } catch (error) {
    console.error("Error al actualizar:", error.message);
  }
}

actualizarPost(1, {
  title: "Título actualizado",
  body: "Contenido completamente nuevo",
  userId: 1,
});

Petición PATCH: actualizar campos específicos

A diferencia de PUT que reemplaza todo el recurso, PATCH solo actualiza los campos que le indicas.

async function actualizarTitulo(id, nuevoTitulo) {
  try {
    const respuesta = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`, {
      method: "PATCH",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ title: nuevoTitulo }),
    });

    if (!respuesta.ok) {
      throw new Error(`Error: ${respuesta.status}`);
    }

    const resultado = await respuesta.json();
    console.log("Título actualizado:", resultado.title);
    return resultado;

  } catch (error) {
    console.error("Error al actualizar el título:", error.message);
  }
}

actualizarTitulo(1, "Solo cambio el título");

Petición DELETE: eliminar un recurso

async function eliminarPost(id) {
  try {
    const respuesta = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`, {
      method: "DELETE",
    });

    if (!respuesta.ok) {
      throw new Error(`Error: ${respuesta.status}`);
    }

    console.log(`Post ${id} eliminado correctamente`);
    return true;

  } catch (error) {
    console.error("Error al eliminar:", error.message);
    return false;
  }
}

eliminarPost(1);

Enviar cabeceras de autenticación

La mayoría de las APIs reales requieren autenticación. El método más común es enviar un token en la cabecera Authorization.

async function obtenerPerfil(token) {
  try {
    const respuesta = await fetch("https://api.ejemplo.com/perfil", {
      method: "GET",
      headers: {
        "Authorization": `Bearer ${token}`,
        "Content-Type": "application/json",
      },
    });

    if (respuesta.status === 401) {
      throw new Error("No autorizado. El token es inválido o expiró.");
    }

    if (!respuesta.ok) {
      throw new Error(`Error: ${respuesta.status}`);
    }

    const perfil = await respuesta.json();
    return perfil;

  } catch (error) {
    console.error("Error:", error.message);
  }
}

Enviar parámetros en la URL

Muchas APIs aceptan parámetros de búsqueda o filtros directamente en la URL. La forma más limpia de construirlas es con URLSearchParams:

async function buscarPosts(usuarioId, limite) {
  const params = new URLSearchParams({
    userId: usuarioId,
    _limit: limite,
  });

  const url = `https://jsonplaceholder.typicode.com/posts?${params}`;

  try {
    const respuesta = await fetch(url);

    if (!respuesta.ok) {
      throw new Error(`Error: ${respuesta.status}`);
    }

    const posts = await respuesta.json();
    console.log(`Posts del usuario ${usuarioId}:`, posts);
    return posts;

  } catch (error) {
    console.error("Error en la búsqueda:", error.message);
  }
}

buscarPosts(1, 5); // Posts del usuario 1, máximo 5 resultados

Hacer varias peticiones en paralelo

Cuando necesitas datos de varios endpoints al mismo tiempo, usar Promise.all() con fetch es mucho más eficiente que esperar una petición tras otra:

async function obtenerDashboard(usuarioId) {
  try {
    const [usuario, posts, tareas] = await Promise.all([
      fetch(`https://jsonplaceholder.typicode.com/users/${usuarioId}`).then((r) => r.json()),
      fetch(`https://jsonplaceholder.typicode.com/posts?userId=${usuarioId}`).then((r) => r.json()),
      fetch(`https://jsonplaceholder.typicode.com/todos?userId=${usuarioId}`).then((r) => r.json()),
    ]);

    console.log("Usuario:", usuario.name);
    console.log("Posts:", posts.length);
    console.log("Tareas:", tareas.length);

    return { usuario, posts, tareas };

  } catch (error) {
    console.error("Error al cargar el dashboard:", error.message);
  }
}

obtenerDashboard(1);

Las tres peticiones se lanzan al mismo tiempo. El tiempo total de espera es el de la petición más lenta, no la suma de todas.

Crear una función fetch reutilizable

En proyectos reales, repetir el mismo bloque de try/catch y la verificación de respuesta.ok en cada llamada genera código duplicado. Lo mejor es crear una función base que centralice esa lógica:

const BASE_URL = "https://jsonplaceholder.typicode.com";

async function peticion(endpoint, opciones = {}) {
  const url = `${BASE_URL}${endpoint}`;

  const configuracion = {
    headers: {
      "Content-Type": "application/json",
      ...opciones.headers,
    },
    ...opciones,
  };

  try {
    const respuesta = await fetch(url, configuracion);

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

    // Algunas respuestas no tienen cuerpo (como DELETE exitoso con 204)
    const contentType = respuesta.headers.get("content-type");
    if (contentType && contentType.includes("application/json")) {
      return await respuesta.json();
    }

    return null;

  } catch (error) {
    console.error(`Error en petición a ${url}:`, error.message);
    throw error;
  }
}

// Uso limpio y sin repetición
const post = await peticion("/posts/1");
const nuevo = await peticion("/posts", { method: "POST", body: JSON.stringify({ title: "Nuevo" }) });
await peticion("/posts/1", { method: "DELETE" });

Manejar correctamente los errores HTTP

Un manejo de errores profesional distingue entre los distintos tipos de fallo que puede devolver una API:

async function obtenerRecurso(url) {
  try {
    const respuesta = await fetch(url);

    switch (respuesta.status) {
      case 200:
        return await respuesta.json();
      case 400:
        throw new Error("Petición incorrecta. Revisa los datos enviados.");
      case 401:
        throw new Error("No autenticado. Inicia sesión nuevamente.");
      case 403:
        throw new Error("No tienes permiso para acceder a este recurso.");
      case 404:
        throw new Error("El recurso solicitado no existe.");
      case 429:
        throw new Error("Demasiadas peticiones. Espera un momento e intenta de nuevo.");
      case 500:
        throw new Error("Error interno del servidor. Intenta más tarde.");
      default:
        if (!respuesta.ok) {
          throw new Error(`Error inesperado: ${respuesta.status}`);
        }
        return await respuesta.json();
    }

  } catch (error) {
    if (error.name === "TypeError") {
      console.error("Error de red: no hay conexión o el servidor no responde.");
    } else {
      console.error("Error:", error.message);
    }
    throw error;
  }
}

Cancelar una petición fetch con AbortController

A veces necesitas cancelar una petición en curso, por ejemplo si el usuario navega a otra página o hace una nueva búsqueda antes de que llegue la respuesta anterior. Para eso existe AbortController:

let controlador = null;

async function buscar(termino) {
  // Cancelar la petición anterior si existe
  if (controlador) {
    controlador.abort();
  }

  controlador = new AbortController();

  try {
    const respuesta = await fetch(
      `https://jsonplaceholder.typicode.com/posts?title=${termino}`,
      { signal: controlador.signal }
    );

    const resultados = await respuesta.json();
    console.log("Resultados:", resultados);
    return resultados;

  } catch (error) {
    if (error.name === "AbortError") {
      console.log("Petición cancelada");
    } else {
      console.error("Error:", error.message);
    }
  }
}

// Simula búsquedas rápidas donde solo importa la última
buscar("sunt");
buscar("qui");
buscar("ea"); // Solo esta petición llegará a completarse

Ejemplo completo: consumir una API real

Para cerrar, un ejemplo completo que consulta la API pública de países y muestra información sobre uno de ellos:

async function obtenerPais(nombre) {
  try {
    const respuesta = await fetch(
      `https://restcountries.com/v3.1/name/${encodeURIComponent(nombre)}`
    );

    if (!respuesta.ok) {
      if (respuesta.status === 404) {
        throw new Error(`No se encontró el país: ${nombre}`);
      }
      throw new Error(`Error: ${respuesta.status}`);
    }

    const datos = await respuesta.json();
    const pais = datos[0];

    const info = {
      nombre: pais.name.common,
      capital: pais.capital?.[0] || "Sin capital",
      poblacion: pais.population.toLocaleString("es"),
      region: pais.region,
      idiomas: Object.values(pais.languages || {}).join(", "),
      moneda: Object.values(pais.currencies || {})
        .map((m) => `${m.name} (${m.symbol})`)
        .join(", "),
    };

    console.log("País:", info.nombre);
    console.log("Capital:", info.capital);
    console.log("Población:", info.poblacion);
    console.log("Región:", info.region);
    console.log("Idiomas:", info.idiomas);
    console.log("Moneda:", info.moneda);

    return info;

  } catch (error) {
    console.error("Error al obtener el país:", error.message);
  }
}

obtenerPais("Colombia");
obtenerPais("España");

Conclusión

La función fetch() es la puerta de entrada a prácticamente cualquier aplicación web moderna. Con ella puedes consumir APIs, enviar formularios, autenticarte y comunicarte con cualquier servicio externo que tenga una interfaz HTTP.

Los puntos más importantes que debes recordar: siempre verifica respuesta.ok porque fetch no lanza error por códigos 4xx o 5xx, siempre usa try/catch para capturar errores de red, usa JSON.stringify() en el body al enviar datos y respuesta.json() al recibirlos, y considera crear una función utilitaria centralizada para evitar repetir la misma lógica en cada llamada.

Si quieres profundizar en los temas relacionados, te recomendamos leer nuestros artículos sobre qué es una API y cómo funciona y sobre qué es JavaScript y para qué se usa, que te darán el contexto necesario para sacarle el máximo partido a lo que aprendiste aquí.

Etiquetas: JavaScript Fetch

¿Te ha gustado esta entrada?

Compártela con tus compañeros para que también sigan aprendiendo.

Comunidad y Comentarios

0 COMENTARIOS

No hay comentarios todavía. Sé el primero en compartir tu opinión.

Escribe tu opinión
Respondiendo a