Blog

RA

Rod Alexanderson

Desarrollador Web

Creando documentación técnica en español para desarrolladores de Latinoamérica.

Más sobre mí →

Suscríbete al Newsletter

Recibe los nuevos artículos directamente en tu email.

Fetch API en JavaScript: Guía Completa

Publicado el 1 de octubre, 2025 • 18 min de lectura

fetch es la forma moderna de obtener datos de servidores en JavaScript. Si necesitas cargar datos de una API, enviar información a un servidor, o subir archivos, fetch es tu herramienta principal.

¿Qué es fetch?

fetch es una función nativa de JavaScript que te permite hacer peticiones HTTP a servidores. En otras palabras, te permite comunicarte con APIs y obtener o enviar información.

Imagina que tu aplicación es un restaurante:

  • Cliente (tu app): "Quiero ver el menú"
  • Mesero (fetch): Va a la cocina y trae el menú
  • Cocina (servidor/API): Prepara y entrega el menú

Fetch es el mesero que va y viene entre tu aplicación y el servidor.

¿Por qué usar fetch?

Antes de fetch, usábamos XMLHttpRequest (una API antigua y complicada):

// Forma antigua con XMLHttpRequest (NO uses esto)
const xhr = new XMLHttpRequest()
xhr.open('GET', 'https://api.ejemplo.com/usuarios')
xhr.onload = function() {
  if (xhr.status === 200) {
    const datos = JSON.parse(xhr.responseText)
    console.log(datos)
  }
}
xhr.onerror = function() {
  console.error('Error')
}
xhr.send()

Complicado, ¿verdad?

Con fetch (forma moderna):

fetch('https://api.ejemplo.com/usuarios')
  .then(response => response.json())
  .then(datos => console.log(datos))
  .catch(error => console.error('Error:', error))

Mucho más limpio y fácil de entender.

ℹ️
Disponibilidad

Fetch está disponible en todos los navegadores modernos y en Node.js desde la versión 18. Es el estándar actual para hacer peticiones HTTP.

Sintaxis básica de fetch

La forma más simple de usar fetch:

fetch(url)

Esto retorna una Promise (promesa) que se resuelve con la respuesta del servidor.

Ejemplo básico con async/await

async function obtenerDatos() {
  const response = await fetch('https://jsonplaceholder.typicode.com/users')
  const datos = await response.json()
  console.log(datos)
}

obtenerDatos()

Desglosando el código:

  1. fetch(url) - Hace la petición al servidor
  2. await - Espera a que llegue la respuesta
  3. response.json() - Convierte la respuesta a formato JSON
  4. Los datos están listos para usar

Ejemplo básico con .then()

Si prefieres usar Promises directamente:

fetch('https://jsonplaceholder.typicode.com/users')
  .then(response => response.json())
  .then(datos => {
    console.log(datos)
  })
  .catch(error => {
    console.error('Error:', error)
  })

Ambas formas funcionan. async/await es generalmente más fácil de leer.

El objeto Response

Cuando haces un fetch, obtienes un objeto Response con información sobre la respuesta del servidor:

async function verificarRespuesta() {
  const response = await fetch('https://api.ejemplo.com/datos')
  
  console.log('Status:', response.status)        // 200, 404, 500, etc.
  console.log('OK:', response.ok)                // true si status 200-299
  console.log('Status Text:', response.statusText) // "OK", "Not Found", etc.
  console.log('Headers:', response.headers)      // Headers de la respuesta
  console.log('URL:', response.url)              // URL completa
}

Propiedades importantes:

PropiedadDescripciónEjemplo
response.oktrue si status 200-299true o false
response.statusCódigo de estado HTTP200, 404, 500
response.statusTextTexto del estado"OK", "Not Found"
response.headersHeaders de respuestaObjeto Headers
response.bodyCuerpo de la respuestaReadableStream

Métodos HTTP: GET, POST, PUT, DELETE

HTTP tiene diferentes "verbos" o métodos para diferentes acciones:

MétodoPropósitoAnalogía
GETObtener datosLeer un libro
POSTCrear datos nuevosEscribir un libro nuevo
PUTActualizar datos existentesEditar un libro completo
PATCHActualizar parte de datosEditar una página del libro
DELETEEliminar datosTirar el libro

GET: Obtener datos

GET es el método por defecto de fetch. Se usa para obtener información:

// Sintaxis simple (método GET por defecto)
async function obtenerUsuarios() {
  const response = await fetch('https://jsonplaceholder.typicode.com/users')
  const usuarios = await response.json()
  console.log(usuarios)
}

// Sintaxis explícita
async function obtenerUsuarios() {
  const response = await fetch('https://jsonplaceholder.typicode.com/users', {
    method: 'GET'
  })
  const usuarios = await response.json()
  console.log(usuarios)
}

Ejemplo real: Obtener un usuario específico

async function obtenerUsuario(id) {
  const response = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`)
  
  if (!response.ok) {
    throw new Error('Usuario no encontrado')
  }
  
  const usuario = await response.json()
  return usuario
}

// Usar la función
obtenerUsuario(1)
  .then(usuario => console.log(usuario.name))
  .catch(error => console.error('Error:', error))

POST: Crear datos nuevos

POST se usa para enviar datos al servidor y crear recursos nuevos:

async function crearUsuario(datosUsuario) {
  const response = await fetch('https://jsonplaceholder.typicode.com/users', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(datosUsuario)
  })
  
  const usuarioCreado = await response.json()
  return usuarioCreado
}

// Usar la función
const nuevoUsuario = {
  name: 'Ana García',
  email: 'ana@ejemplo.com',
  username: 'anagarcia'
}

crearUsuario(nuevoUsuario)
  .then(usuario => console.log('Usuario creado:', usuario))
  .catch(error => console.error('Error:', error))

Elementos importantes de POST:

  1. method: 'POST' - Especifica el método
  2. headers - Le dice al servidor qué tipo de datos envías
  3. body - Los datos que envías (convertidos a string con JSON.stringify)

PUT: Actualizar datos completos

PUT se usa para actualizar un recurso completo:

async function actualizarUsuario(id, datosActualizados) {
  const response = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`, {
    method: 'PUT',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(datosActualizados)
  })
  
  const usuarioActualizado = await response.json()
  return usuarioActualizado
}

// Usar la función
const datosActualizados = {
  id: 1,
  name: 'Ana García Actualizada',
  email: 'ana.nueva@ejemplo.com',
  username: 'anagarcia'
}

actualizarUsuario(1, datosActualizados)
  .then(usuario => console.log('Usuario actualizado:', usuario))

PATCH: Actualizar datos parciales

PATCH se usa para actualizar solo algunos campos:

async function actualizarEmail(id, nuevoEmail) {
  const response = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`, {
    method: 'PATCH',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      email: nuevoEmail  // Solo actualizar el email
    })
  })
  
  const usuarioActualizado = await response.json()
  return usuarioActualizado
}

// Usar la función
actualizarEmail(1, 'nuevo@ejemplo.com')
  .then(usuario => console.log('Email actualizado:', usuario))

Diferencia entre PUT y PATCH:

  • PUT: Reemplaza el recurso completo (debes enviar todos los campos)
  • PATCH: Actualiza solo los campos que especifiques

DELETE: Eliminar datos

DELETE se usa para eliminar recursos:

async function eliminarUsuario(id) {
  const response = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`, {
    method: 'DELETE'
  })
  
  if (response.ok) {
    console.log('Usuario eliminado exitosamente')
  }
}

// Usar la función
eliminarUsuario(1)
  .then(() => console.log('Usuario eliminado'))
  .catch(error => console.error('Error:', error))

Headers (Encabezados)

Los headers son información adicional que envías con tu petición. Piénsalos como el sobre de una carta que contiene datos sobre el remitente, destinatario, etc.

Headers comunes

const response = await fetch('https://api.ejemplo.com/datos', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',      // Tipo de datos que envías
    'Accept': 'application/json',            // Tipo de datos que aceptas
    'Authorization': 'Bearer tu-token-aqui', // Token de autenticación
    'X-Custom-Header': 'valor-personalizado' // Header personalizado
  },
  body: JSON.stringify({ dato: 'valor' })
})

Headers más usados:

HeaderPropósitoEjemplo
Content-TypeTipo de datos que envíasapplication/json
AcceptTipo de datos que aceptasapplication/json
AuthorizationToken de autenticaciónBearer abc123...
User-AgentInformación del navegadorMozilla/5.0...

Leer headers de la respuesta

async function verHeaders() {
  const response = await fetch('https://api.ejemplo.com/datos')
  
  // Obtener un header específico
  const contentType = response.headers.get('content-type')
  console.log('Content-Type:', contentType)
  
  // Iterar sobre todos los headers
  response.headers.forEach((valor, nombre) => {
    console.log(`${nombre}: ${valor}`)
  })
}

Diferentes tipos de datos

JSON (lo más común)

async function enviarJSON() {
  const response = await fetch('https://api.ejemplo.com/usuarios', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      nombre: 'Ana',
      edad: 25
    })
  })
  
  const datos = await response.json()
  console.log(datos)
}

Texto plano

async function obtenerTexto() {
  const response = await fetch('https://ejemplo.com/archivo.txt')
  const texto = await response.text()
  console.log(texto)
}

FormData (para formularios y archivos)

async function subirArchivo(archivo) {
  const formData = new FormData()
  formData.append('archivo', archivo)
  formData.append('descripcion', 'Mi archivo')
  
  const response = await fetch('https://api.ejemplo.com/upload', {
    method: 'POST',
    body: formData  // NO pongas Content-Type, se añade automáticamente
  })
  
  const resultado = await response.json()
  console.log(resultado)
}

// Usar con un input de archivo
const input = document.querySelector('input[type="file"]')
input.addEventListener('change', (e) => {
  const archivo = e.target.files[0]
  subirArchivo(archivo)
})

Blob (para imágenes, videos, etc.)

async function descargarImagen() {
  const response = await fetch('https://ejemplo.com/imagen.jpg')
  const blob = await response.blob()
  
  // Crear URL temporal para la imagen
  const url = URL.createObjectURL(blob)
  
  // Mostrar la imagen
  const img = document.createElement('img')
  img.src = url
  document.body.appendChild(img)
}

Manejo de errores

Fetch tiene una peculiaridad: solo rechaza la promesa en errores de red. Si el servidor responde con error 404 o 500, fetch NO lo considera error.

Verificar response.ok

async function obtenerDatos() {
  try {
    const response = await fetch('https://api.ejemplo.com/datos')
    
    // Verificar si la respuesta fue exitosa
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`)
    }
    
    const datos = await response.json()
    return datos
  } catch (error) {
    console.error('Error:', error.message)
    throw error
  }
}

Manejo completo de errores

async function obtenerUsuario(id) {
  try {
    const response = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`)
    
    // Error 404, 500, etc.
    if (!response.ok) {
      if (response.status === 404) {
        throw new Error('Usuario no encontrado')
      } else if (response.status === 500) {
        throw new Error('Error del servidor')
      } else {
        throw new Error(`Error: ${response.status}`)
      }
    }
    
    const usuario = await response.json()
    return usuario
    
  } catch (error) {
    // Errores de red (sin internet, servidor caído, etc.)
    if (error instanceof TypeError) {
      console.error('Error de red:', error.message)
      throw new Error('No se pudo conectar al servidor')
    }
    
    // Otros errores
    console.error('Error:', error.message)
    throw error
  }
}

// Usar la función
obtenerUsuario(999)
  .then(usuario => console.log(usuario))
  .catch(error => console.error('Falló:', error.message))
⚠️
Fetch no rechaza en errores HTTP

A diferencia de otras librerías, fetch solo rechaza la promesa en errores de red (sin internet, DNS falló, etc.). Respuestas con status 404, 500, etc. se consideran "exitosas" y debes verificar response.ok manualmente.

Opciones de configuración

Fetch acepta un segundo parámetro con opciones:

const response = await fetch(url, {
  method: 'GET',           // GET, POST, PUT, DELETE, etc.
  headers: {},             // Headers personalizados
  body: null,              // Datos a enviar (no en GET)
  mode: 'cors',            // cors, no-cors, same-origin
  credentials: 'include',  // omit, same-origin, include
  cache: 'default',        // default, no-cache, reload, force-cache
  redirect: 'follow',      // follow, error, manual
  referrer: 'client',      // URL del referrer
  signal: null             // AbortSignal para cancelar
})

Opciones comunes explicadas

mode:

// cors: Permite peticiones a otros dominios (lo más común)
fetch('https://api.ejemplo.com/datos', {
  mode: 'cors'
})

// same-origin: Solo permite peticiones al mismo dominio
fetch('/api/datos', {
  mode: 'same-origin'
})

credentials:

// include: Envía cookies incluso a otros dominios
fetch('https://api.ejemplo.com/datos', {
  credentials: 'include'
})

// same-origin: Solo envía cookies al mismo dominio (default)
fetch('/api/datos', {
  credentials: 'same-origin'
})

// omit: No envía cookies
fetch('https://api.ejemplo.com/datos', {
  credentials: 'omit'
})

cache:

// no-cache: Siempre pide datos frescos
fetch('https://api.ejemplo.com/datos', {
  cache: 'no-cache'
})

// force-cache: Usa caché si está disponible
fetch('https://api.ejemplo.com/datos', {
  cache: 'force-cache'
})

Cancelar peticiones con AbortController

A veces necesitas cancelar una petición (usuario cambia de página, timeout, etc.):

async function buscarConTimeout() {
  // Crear un controlador para cancelar
  const controller = new AbortController()
  const signal = controller.signal
  
  // Cancelar después de 5 segundos
  const timeout = setTimeout(() => controller.abort(), 5000)
  
  try {
    const response = await fetch('https://api.ejemplo.com/datos', {
      signal: signal  // Pasar el signal
    })
    
    clearTimeout(timeout)  // Cancelar el timeout si termina a tiempo
    const datos = await response.json()
    return datos
    
  } catch (error) {
    if (error.name === 'AbortError') {
      console.log('Petición cancelada')
    } else {
      console.error('Error:', error)
    }
  }
}

Ejemplo práctico: Búsqueda con cancelación

let controladorActual = null

async function buscar(query) {
  // Cancelar búsqueda anterior si existe
  if (controladorActual) {
    controladorActual.abort()
  }
  
  // Crear nuevo controlador
  controladorActual = new AbortController()
  
  try {
    const response = await fetch(`/api/buscar?q=${query}`, {
      signal: controladorActual.signal
    })
    
    const resultados = await response.json()
    mostrarResultados(resultados)
    
  } catch (error) {
    if (error.name === 'AbortError') {
      console.log('Búsqueda cancelada')
    }
  }
}

// En un input de búsqueda
input.addEventListener('input', (e) => {
  buscar(e.target.value)
})

Esto cancela búsquedas anteriores si el usuario sigue escribiendo.

Ejemplos prácticos completos

Ejemplo 1: Cargar y mostrar lista de usuarios

async function cargarUsuarios() {
  const contenedor = document.getElementById('usuarios')
  contenedor.innerHTML = '<p>Cargando...</p>'
  
  try {
    const response = await fetch('https://jsonplaceholder.typicode.com/users')
    
    if (!response.ok) {
      throw new Error('Error al cargar usuarios')
    }
    
    const usuarios = await response.json()
    
    // Limpiar contenedor
    contenedor.innerHTML = ''
    
    // Crear HTML para cada usuario
    usuarios.forEach(usuario => {
      const div = document.createElement('div')
      div.innerHTML = `
        <h3>${usuario.name}</h3>
        <p>Email: ${usuario.email}</p>
        <p>Ciudad: ${usuario.address.city}</p>
        <hr>
      `
      contenedor.appendChild(div)
    })
    
  } catch (error) {
    contenedor.innerHTML = `<p>Error: ${error.message}</p>`
  }
}

// Llamar al cargar la página
cargarUsuarios()

Ejemplo 2: Formulario de contacto

async function enviarFormulario(evento) {
  evento.preventDefault()
  
  // Obtener datos del formulario
  const formulario = evento.target
  const datos = {
    nombre: formulario.nombre.value,
    email: formulario.email.value,
    mensaje: formulario.mensaje.value
  }
  
  // Deshabilitar botón mientras envía
  const boton = formulario.querySelector('button')
  boton.disabled = true
  boton.textContent = 'Enviando...'
  
  try {
    const response = await fetch('/api/contacto', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(datos)
    })
    
    if (!response.ok) {
      throw new Error('Error al enviar mensaje')
    }
    
    const resultado = await response.json()
    alert('Mensaje enviado exitosamente')
    formulario.reset()
    
  } catch (error) {
    alert('Error: ' + error.message)
  } finally {
    boton.disabled = false
    boton.textContent = 'Enviar'
  }
}

// Conectar con el formulario
document.getElementById('formulario-contacto')
  .addEventListener('submit', enviarFormulario)

Ejemplo 3: Subir imagen con preview

async function subirImagen(archivo) {
  // Mostrar preview
  const preview = document.getElementById('preview')
  const reader = new FileReader()
  reader.onload = (e) => {
    preview.src = e.target.result
  }
  reader.readAsDataURL(archivo)
  
  // Preparar FormData
  const formData = new FormData()
  formData.append('imagen', archivo)
  formData.append('titulo', 'Mi imagen')
  
  try {
    const response = await fetch('/api/upload', {
      method: 'POST',
      body: formData
    })
    
    if (!response.ok) {
      throw new Error('Error al subir imagen')
    }
    
    const resultado = await response.json()
    console.log('Imagen subida:', resultado.url)
    alert('Imagen subida exitosamente')
    
  } catch (error) {
    alert('Error: ' + error.message)
  }
}

// Conectar con input file
document.getElementById('input-imagen')
  .addEventListener('change', (e) => {
    const archivo = e.target.files[0]
    if (archivo) {
      subirImagen(archivo)
    }
  })

Ejemplo 4: CRUD completo

// CRUD = Create, Read, Update, Delete

const API_URL = 'https://jsonplaceholder.typicode.com/users'

// CREATE - Crear usuario
async function crearUsuario(datos) {
  const response = await fetch(API_URL, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(datos)
  })
  return await response.json()
}

// READ - Leer usuarios
async function obtenerUsuarios() {
  const response = await fetch(API_URL)
  return await response.json()
}

// READ - Leer un usuario
async function obtenerUsuario(id) {
  const response = await fetch(`${API_URL}/${id}`)
  return await response.json()
}

// UPDATE - Actualizar usuario
async function actualizarUsuario(id, datos) {
  const response = await fetch(`${API_URL}/${id}`, {
    method: 'PUT',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(datos)
  })
  return await response.json()
}

// DELETE - Eliminar usuario
async function eliminarUsuario(id) {
  const response = await fetch(`${API_URL}/${id}`, {
    method: 'DELETE'
  })
  return response.ok
}

// Usar las funciones
async function ejemploCRUD() {
  // Crear
  const nuevoUsuario = await crearUsuario({
    name: 'Ana García',
    email: 'ana@ejemplo.com'
  })
  console.log('Usuario creado:', nuevoUsuario)
  
  // Leer todos
  const usuarios = await obtenerUsuarios()
  console.log('Usuarios:', usuarios.length)
  
  // Leer uno
  const usuario = await obtenerUsuario(1)
  console.log('Usuario 1:', usuario.name)
  
  // Actualizar
  const actualizado = await actualizarUsuario(1, {
    name: 'Ana García Actualizada',
    email: 'ana.nueva@ejemplo.com'
  })
  console.log('Usuario actualizado:', actualizado)
  
  // Eliminar
  const eliminado = await eliminarUsuario(1)
  console.log('Usuario eliminado:', eliminado)
}

Patrones útiles

Patrón 1: Función wrapper reutilizable

async function fetchJSON(url, opciones = {}) {
  try {
    const response = await fetch(url, {
      headers: {
        'Content-Type': 'application/json',
        ...opciones.headers
      },
      ...opciones
    })
    
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`)
    }
    
    return await response.json()
  } catch (error) {
    console.error('Error en fetch:', error)
    throw error
  }
}

// Usar la función wrapper
const usuarios = await fetchJSON('https://api.ejemplo.com/usuarios')
const usuario = await fetchJSON('https://api.ejemplo.com/usuarios/1')

Patrón 2: Retry automático

async function fetchConReintentos(url, opciones = {}, intentos = 3) {
  for (let i = 0; i < intentos; i++) {
    try {
      const response = await fetch(url, opciones)
      
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`)
      }
      
      return await response.json()
    } catch (error) {
      // Si es el último intento, lanzar el error
      if (i === intentos - 1) throw error
      
      // Esperar antes de reintentar (backoff exponencial)
      await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)))
    }
  }
}

// Usar con reintentos
const datos = await fetchConReintentos('https://api.ejemplo.com/datos', {}, 3)

Patrón 3: Cache simple

const cache = new Map()

async function fetchConCache(url, tiempoCache = 60000) {
  // Verificar si está en caché y no ha expirado
  if (cache.has(url)) {
    const { datos, timestamp } = cache.get(url)
    if (Date.now() - timestamp < tiempoCache) {
      console.log('Usando caché')
      return datos
    }
  }
  
  // Hacer fetch y guardar en caché
  console.log('Haciendo fetch')
  const response = await fetch(url)
  const datos = await response.json()
  
  cache.set(url, {
    datos,
    timestamp: Date.now()
  })
  
  return datos
}

// Primera llamada: hace fetch
const datos1 = await fetchConCache('https://api.ejemplo.com/datos')

// Segunda llamada (dentro de 60s): usa caché
const datos2 = await fetchConCache('https://api.ejemplo.com/datos')

Patrón 4: Progreso de descarga

async function descargarConProgreso(url) {
  const response = await fetch(url)
  const total = response.headers.get('content-length')
  
  let descargado = 0
  const chunks = []
  
  const reader = response.body.getReader()
  
  while (true) {
    const { done, value } = await reader.read()
    
    if (done) break
    
    chunks.push(value)
    descargado += value.length
    
    // Calcular progreso
    const progreso = total ? (descargado / total) * 100 : 0
    console.log(`Descargado: ${progreso.toFixed(2)}%`)
  }
  
  // Combinar todos los chunks
  const blob = new Blob(chunks)
  return blob
}

// Usar con una imagen
descargarConProgreso('https://ejemplo.com/imagen-grande.jpg')
  .then(blob => {
    const url = URL.createObjectURL(blob)
    console.log('Descarga completa:', url)
  })

Errores comunes

Error 1: Olvidar await en .json()

// ❌ Incorrecto
async function obtenerDatos() {
  const response = await fetch('url')
  const datos = response.json()  // Falta await
  console.log(datos)  // Imprime una Promise, no los datos
}

// ✓ Correcto
async function obtenerDatos() {
  const response = await fetch('url')
  const datos = await response.json()
  console.log(datos)
}

Error 2: No verificar response.ok

// ❌ Incorrecto
async function obtenerDatos() {
  const response = await fetch('url')
  const datos = await response.json()
  return datos
}

// ✓ Correcto
async function obtenerDatos() {
  const response = await fetch('url')
  
  if (!response.ok) {
    throw new Error('Error en la petición')
  }
  
  const datos = await response.json()
  return datos
}

Error 3: Enviar body en GET

// ❌ Incorrecto: GET no debe tener body
fetch('url', {
  method: 'GET',
  body: JSON.stringify({ dato: 'valor' })
})

// ✓ Correcto: Usa query parameters
fetch('url?dato=valor', {
  method: 'GET'
})

Error 4: Olvidar Content-Type en POST

// ❌ Incorrecto
fetch('url', {
  method: 'POST',
  body: JSON.stringify({ dato: 'valor' })
  // Falta Content-Type header
})

// ✓ Correcto
fetch('url', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({ dato: 'valor' })
})

Error 5: Usar .json() en respuestas no-JSON

// ❌ Incorrecto: Si la respuesta es texto plano
async function obtenerTexto() {
  const response = await fetch('archivo.txt')
  const datos = await response.json()  // Error: no es JSON
}

// ✓ Correcto
async function obtenerTexto() {
  const response = await fetch('archivo.txt')
  const texto = await response.text()  // Usar .text() para texto plano
}

Fetch en diferentes entornos

En Node.js

Fetch está disponible nativamente desde Node.js 18:

// Node.js 18+
const response = await fetch('https://api.ejemplo.com/datos')
const datos = await response.json()

Para versiones anteriores, instala node-fetch:

npm install node-fetch
// Node.js < 18
import fetch from 'node-fetch'

const response = await fetch('https://api.ejemplo.com/datos')
const datos = await response.json()

En NextJS (Server Components)

// app/page.tsx (Server Component)
export default async function Page() {
  const response = await fetch('https://api.ejemplo.com/productos')
  const productos = await response.json()
  
  return (
    <div>
      {productos.map(p => (
        <div key={p.id}>{p.nombre}</div>
      ))}
    </div>
  )
}

En React (Client Components)

'use client'

import { useState, useEffect } from 'react'

export default function Productos() {
  const [productos, setProductos] = useState([])
  const [cargando, setCargando] = useState(true)
  
  useEffect(() => {
    async function cargar() {
      const response = await fetch('https://api.ejemplo.com/productos')
      const datos = await response.json()
      setProductos(datos)
      setCargando(false)
    }
    
    cargar()
  }, [])
  
  if (cargando) return <div>Cargando...</div>
  
  return (
    <div>
      {productos.map(p => (
        <div key={p.id}>{p.nombre}</div>
      ))}
    </div>
  )
}

Resumen

Puntos clave sobre Fetch:

  1. Fetch es la forma moderna de hacer peticiones HTTP
  2. Retorna una Promise que se resuelve con un objeto Response
  3. Debes llamar .json(), .text(), o .blob() para obtener los datos
  4. Siempre verifica response.ok - fetch no rechaza en errores HTTP
  5. Usa method para especificar GET, POST, PUT, DELETE
  6. Usa headers para enviar información adicional
  7. Usa body para enviar datos (no en GET)
  8. Siempre maneja errores con try/catch
  9. Puedes cancelar peticiones con AbortController
  10. Funciona en navegadores y Node.js 18+

Tabla de referencia rápida:

AcciónCódigo
GET básicofetch(url)
POST con JSONfetch(url, { method: 'POST', body: JSON.stringify(data) })
Obtener JSONawait response.json()
Obtener textoawait response.text()
Verificar éxitoif (!response.ok) throw new Error()
Con headersfetch(url, { headers: { 'Authorization': 'Bearer token' } })
Cancelarfetch(url, { signal: controller.signal })

En el próximo artículo veremos Axios, una alternativa popular a fetch con funcionalidades adicionales.


Recursos adicionales:


Sobre el autor: Desarrollador especializado en JavaScript, React y NextJS. Creo contenido educativo en español para ayudar a la comunidad de desarrolladores.