Saltar al contenido
api-rest.js · devschool

Crear una API REST con Express

Lección 38 de 38 · 7 min de lectura · Actualizado el

En esta lección
  1. Métodos HTTP y operaciones
  2. Diseñar buenas URLs
  3. La API de tareas
  4. Por qué todo en el mismo servidor
  5. Siguientes pasos

Una API (Application Programming Interface) es la forma en que un programa ofrece sus servicios a otros programas. Una API web es un servidor que, en lugar de páginas HTML para personas, devuelve datos (normalmente JSON) para otros programas: tu página web, una app móvil, otro servidor…

REST es el estilo más extendido para diseñar APIs web. Sus ideas principales:

  • Todo gira en torno a recursos (tareas, usuarios, productos…), y cada recurso tiene su URL.
  • Lo que quieres hacer con el recurso lo indica el método HTTP, no la URL.
  • Las respuestas llevan el código de estado adecuado.
  • El servidor no guarda “en qué paso va” cada cliente: cada petición lleva toda la información necesaria.

Métodos HTTP y operaciones

Las cuatro operaciones básicas sobre datos (crear, leer, actualizar y borrar, lo que se conoce como CRUD) se corresponden con métodos HTTP:

OperaciónMétodoURLRespuesta típica
Listar todas las tareasGET/api/tareas200 + array
Ver una tareaGET/api/tareas/7200 + objeto, o 404
Crear una tareaPOST/api/tareas201 + la tarea creada
Modificar una tareaPUT (entera) o PATCH (parte)/api/tareas/7200 + la tarea, o 404
Borrar una tareaDELETE/api/tareas/7204 sin contenido, o 404

Fíjate: la URL es la misma para ver, modificar y borrar la tarea 7. Lo que cambia es el método.

Diseñar buenas URLs

Una URL bien diseñada debería poder durar años, aunque cambies de lenguaje, de framework o de servidor. Algunas reglas:

  • Sin detalles técnicos. Nada de .php, .asp o .js en la URL.
    • ✓ /api/tareas
    • ✗ /api/tareas.php
  • Sin verbos: la acción la dice el método HTTP.
    • ✓ DELETE /api/tareas/7
    • ✗ GET /api/borrarTarea?id=7
  • Nombres en plural para las colecciones: /api/tareas, /api/usuarios.
  • Sin palabras que no aportan nada. Si todas tus URLs empiezan por /home/app/, sobra.
    • ✓ /api/tareas
    • ✗ /home/app/v1/sistema/api/tareas
  • Un único estilo para separar palabras. Lo más recomendable: minúsculas y guiones (/api/lineas-pedido). Lo importante es no mezclar estilos.
  • Solo caracteres ingleses básicos: sin espacios, tildes ni eñes.
    • ✓ /api/anios/2026
    • ✗ /api/años/2026
  • Relaciones anidadas cuando tenga sentido: /api/usuarios/3/tareas (las tareas del usuario 3).

La API de tareas

Vamos a construir una API REST completa. Para centrarnos en la API, guardaremos las tareas en un array en memoria (se perderán al reiniciar el servidor; en un proyecto real irían a una base de datos).

Estructura del proyecto:

api-tareas/
├── package.json        ← con "type": "module" y express como dependencia
├── servidor.js
└── public/
    ├── index.html
    └── js/
        └── app.js

El servidor: servidor.js

import express from "express";
import path from "node:path";

const app = express();
const puerto = process.env.PORT || 3000;

app.use(express.json());
app.use(express.static(path.join(import.meta.dirname, "public")));

// "Base de datos" en memoria
let tareas = [
  { id: 1, titulo: "Aprender HTML", hecha: true },
  { id: 2, titulo: "Aprender CSS", hecha: false },
];
let siguienteId = 3;

// Listar
app.get("/api/tareas", (req, res) => {
  res.json(tareas);
});

// Ver una
app.get("/api/tareas/:id", (req, res) => {
  const tarea = tareas.find((t) => t.id === Number(req.params.id));
  if (!tarea) return res.status(404).json({ error: "Tarea no encontrada" });
  res.json(tarea);
});

// Crear
app.post("/api/tareas", (req, res) => {
  const titulo = req.body?.titulo?.trim();
  if (!titulo) return res.status(400).json({ error: "El título es obligatorio" });

  const nueva = { id: siguienteId++, titulo, hecha: false };
  tareas.push(nueva);
  res.status(201).json(nueva);
});

// Modificar (parcialmente)
app.patch("/api/tareas/:id", (req, res) => {
  const tarea = tareas.find((t) => t.id === Number(req.params.id));
  if (!tarea) return res.status(404).json({ error: "Tarea no encontrada" });

  if (typeof req.body.titulo === "string") tarea.titulo = req.body.titulo.trim();
  if (typeof req.body.hecha === "boolean") tarea.hecha = req.body.hecha;
  res.json(tarea);
});

// Borrar
app.delete("/api/tareas/:id", (req, res) => {
  const antes = tareas.length;
  tareas = tareas.filter((t) => t.id !== Number(req.params.id));
  if (tareas.length === antes) return res.status(404).json({ error: "Tarea no encontrada" });
  res.status(204).end();
});

// Errores
app.use((req, res) => res.status(404).json({ error: "Ruta no encontrada" }));
app.use((err, req, res, next) => {
  console.error(err);
  res.status(500).json({ error: "Error en el servidor" });
});

app.listen(puerto, () => console.log(`API en http://localhost:${puerto}`));

Detalles importantes:

  • Los parámetros de la URL llegan como texto: Number(req.params.id) antes de comparar con ===.
  • Nunca te fíes de lo que envía el cliente: comprueba que existe titulo, que es texto y que no está vacío. Si no, responde 400.
  • return res.status(...)... termina el manejador. Sin el return, el código seguiría y Express intentaría responder dos veces (verías el error Cannot set headers after they are sent).

Prueba la API antes de tocar el cliente: abre http://localhost:3000/api/tareas en el navegador, y usa Postman, Thunder Client o curl para el resto de métodos.

La página: public/index.html

<!doctype html>
<html lang="es">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Mis tareas</title>
  <script type="module" src="js/app.js"></script>
</head>
<body>
  <h1>Mis tareas</h1>
  <form id="formulario">
    <label for="titulo">Nueva tarea</label>
    <input id="titulo" name="titulo" required>
    <button>Añadir</button>
  </form>
  <p id="estado" aria-live="polite"></p>
  <ul id="lista"></ul>
</body>
</html>

El cliente: public/js/app.js

const lista = document.querySelector("#lista");
const estado = document.querySelector("#estado");
const formulario = document.querySelector("#formulario");

// --- Comunicación con la API ---

async function api(ruta, opciones = {}) {
  const respuesta = await fetch(`/api${ruta}`, {
    headers: { "Content-Type": "application/json" },
    ...opciones,
  });
  if (!respuesta.ok) {
    const cuerpo = await respuesta.json().catch(() => ({}));
    throw new Error(cuerpo.error || `Error ${respuesta.status}`);
  }
  return respuesta.status === 204 ? null : respuesta.json();
}

// --- Interfaz ---

function pintar(tareas) {
  lista.replaceChildren(
    ...tareas.map((tarea) => {
      const li = document.createElement("li");
      li.dataset.id = tarea.id;

      const casilla = document.createElement("input");
      casilla.type = "checkbox";
      casilla.checked = tarea.hecha;
      casilla.className = "marcar";
      casilla.setAttribute("aria-label", `Marcar "${tarea.titulo}" como hecha`);

      const texto = document.createElement("span");
      texto.textContent = ` ${tarea.titulo} `;

      const borrar = document.createElement("button");
      borrar.textContent = "Borrar";
      borrar.className = "borrar";

      li.append(casilla, texto, borrar);
      return li;
    })
  );
}

async function recargar() {
  try {
    pintar(await api("/tareas"));
    estado.textContent = "";
  } catch (error) {
    estado.textContent = `No se pudieron cargar las tareas: ${error.message}`;
  }
}

formulario.addEventListener("submit", async (evento) => {
  evento.preventDefault();
  const titulo = formulario.titulo.value;
  try {
    await api("/tareas", { method: "POST", body: JSON.stringify({ titulo }) });
    formulario.reset();
    await recargar();
  } catch (error) {
    estado.textContent = error.message;
  }
});

// Delegación de eventos: un solo manejador para todas las tareas
lista.addEventListener("click", async (evento) => {
  const id = evento.target.closest("li")?.dataset.id;
  if (!id) return;

  try {
    if (evento.target.matches(".borrar")) {
      await api(`/tareas/${id}`, { method: "DELETE" });
    } else if (evento.target.matches(".marcar")) {
      await api(`/tareas/${id}`, {
        method: "PATCH",
        body: JSON.stringify({ hecha: evento.target.checked }),
      });
    } else {
      return;
    }
    await recargar();
  } catch (error) {
    estado.textContent = error.message;
  }
});

recargar();

Arranca el servidor con node servidor.js y abre http://localhost:3000. Tienes una aplicación completa: HTML, CSS (añádele el tuyo), JavaScript en el cliente, una API REST en el servidor y comunicación entre ambos con JSON.

Por qué todo en el mismo servidor

Fíjate en que fetch usa rutas como /api/tareas, sin dominio ni puerto. Funciona porque la página y la API las sirve el mismo servidor (el mismo origen), así que no hay ningún problema con la política del mismo origen ni con CORS. Es la configuración más sencilla y más segura, y la que te recomendamos para tus proyectos.

Si abrieras index.html con doble clic desde tu disco, la página no podría hablar con la API (y los módulos ni siquiera se cargarían). Ábrela siempre a través del servidor: http://localhost:3000.

Siguientes pasos

  • Base de datos: sustituye el array por una base de datos real, como SQLite o PostgreSQL. Lo que aprendes en el curso de SQL es justo lo que necesitas.
  • Validación: librerías como Zod comprueban los datos que llegan de forma más completa.
  • Autenticación: que cada usuario solo vea y modifique sus tareas.
  • Despliegue: publica tu API en un servicio como Render, Railway o Fly.io.

Pon a prueba lo que has aprendido

[JavaScript] En una API REST, ¿qué método se usa para crear un recurso nuevo?

[JavaScript] ¿Qué URL sigue mejor las recomendaciones REST para borrar la tarea 7?

¿Te ha quedado claro? Márcala y verás tu progreso en el explorador.