Diseño de rutas en Express para una API REST simple

Capítulo 5

Tiempo estimado de lectura: 8 minutos

+ Ejercicio

Qué significa “diseñar rutas” en una API REST

En una API REST simple, el diseño de rutas consiste en definir recursos (por ejemplo, usuarios) y exponer operaciones sobre ellos mediante métodos HTTP. La idea es que la URL represente el recurso y el método represente la acción.

AcciónMétodoRuta típicaResultado esperado
ListarGET/api/usersDevuelve colección
Obtener unoGET/api/users/:idDevuelve un recurso
CrearPOST/api/usersCrea y devuelve el recurso
Actualizar completoPUT/api/users/:idReemplaza el recurso
Actualizar parcialPATCH/api/users/:idModifica campos
EliminarDELETE/api/users/:idElimina el recurso

Express Router: separar rutas por recurso

express.Router() permite agrupar rutas relacionadas (por ejemplo, todo lo de users) y montarlas bajo un prefijo como /api/users. Esto mejora la organización y hace más fácil crecer la API.

Paso 1: crear un router para usuarios

Crea un archivo como routes/users.routes.js:

const express = require('express');
const router = express.Router();

// Datos en memoria (simulan una base de datos)
let users = [
  { id: 1, name: 'Ada Lovelace', email: 'ada@example.com' },
  { id: 2, name: 'Alan Turing', email: 'alan@example.com' }
];
let nextId = 3;

module.exports = { router, usersRef: () => users, setUsers: (u) => { users = u; }, nextIdRef: () => nextId, incNextId: () => { nextId += 1; } };

En un proyecto real, normalmente exportarías solo router y la lógica viviría en controladores/servicios. Aquí mantenemos todo junto para enfocarnos en el diseño REST y el CRUD en memoria.

Paso 2: montar el router en la app

En tu archivo principal (por ejemplo app.js o server.js), monta el router:

Continúa en nuestra aplicación.
  • Escuche el audio con la pantalla apagada.
  • Obtenga un certificado al finalizar.
  • ¡Más de 5000 cursos para que explores!
O continúa leyendo más abajo...
Download App

Descargar la aplicación

const express = require('express');
const app = express();

app.use(express.json());

const usersRouter = require('./routes/users.router');
app.use('/api/users', usersRouter);

module.exports = app;

Nota: el nombre del archivo en el ejemplo de montaje asume que crearás routes/users.router.js. Ajusta el nombre según tu estructura.

CRUD completo con datos en memoria

A continuación se muestra un router funcional con endpoints REST para usuarios, validaciones mínimas y errores consistentes. Crea el archivo routes/users.router.js:

const express = require('express');
const router = express.Router();

// “Base de datos” en memoria
let users = [
  { id: 1, name: 'Ada Lovelace', email: 'ada@example.com' },
  { id: 2, name: 'Alan Turing', email: 'alan@example.com' }
];
let nextId = 3;

// Helpers
function toInt(value) {
  const n = Number(value);
  return Number.isInteger(n) ? n : null;
}

function sendError(res, status, code, message, details) {
  const payload = { error: { code, message } };
  if (details) payload.error.details = details;
  return res.status(status).json(payload);
}

function validateRequiredFields(body, required) {
  const missing = required.filter((key) => body[key] === undefined || body[key] === null || body[key] === '');
  return missing;
}

function findUserIndexById(id) {
  return users.findIndex((u) => u.id === id);
}

// GET /api/users - listar
router.get('/', (req, res) => {
  res.status(200).json({ data: users });
});

// GET /api/users/:id - obtener uno
router.get('/:id', (req, res) => {
  const id = toInt(req.params.id);
  if (id === null) {
    return sendError(res, 400, 'INVALID_ID', 'El parámetro id debe ser un entero.');
  }

  const user = users.find((u) => u.id === id);
  if (!user) {
    return sendError(res, 404, 'NOT_FOUND', 'Usuario no encontrado.');
  }

  res.status(200).json({ data: user });
});

// POST /api/users - crear
router.post('/', (req, res) => {
  const missing = validateRequiredFields(req.body, ['name', 'email']);
  if (missing.length > 0) {
    return sendError(res, 400, 'VALIDATION_ERROR', 'Faltan campos requeridos.', { missing });
  }

  const name = String(req.body.name).trim();
  const email = String(req.body.email).trim();

  if (name.length < 2) {
    return sendError(res, 400, 'VALIDATION_ERROR', 'El nombre debe tener al menos 2 caracteres.', { field: 'name' });
  }
  if (!email.includes('@')) {
    return sendError(res, 400, 'VALIDATION_ERROR', 'Email inválido.', { field: 'email' });
  }

  const emailExists = users.some((u) => u.email.toLowerCase() === email.toLowerCase());
  if (emailExists) {
    return sendError(res, 409, 'CONFLICT', 'Ya existe un usuario con ese email.', { field: 'email' });
  }

  const newUser = { id: nextId++, name, email };
  users.push(newUser);

  res.status(201).json({ data: newUser });
});

// PUT /api/users/:id - reemplazo completo
router.put('/:id', (req, res) => {
  const id = toInt(req.params.id);
  if (id === null) {
    return sendError(res, 400, 'INVALID_ID', 'El parámetro id debe ser un entero.');
  }

  const idx = findUserIndexById(id);
  if (idx === -1) {
    return sendError(res, 404, 'NOT_FOUND', 'Usuario no encontrado.');
  }

  const missing = validateRequiredFields(req.body, ['name', 'email']);
  if (missing.length > 0) {
    return sendError(res, 400, 'VALIDATION_ERROR', 'En PUT debes enviar todos los campos requeridos.', { missing });
  }

  const name = String(req.body.name).trim();
  const email = String(req.body.email).trim();

  if (name.length < 2) {
    return sendError(res, 400, 'VALIDATION_ERROR', 'El nombre debe tener al menos 2 caracteres.', { field: 'name' });
  }
  if (!email.includes('@')) {
    return sendError(res, 400, 'VALIDATION_ERROR', 'Email inválido.', { field: 'email' });
  }

  const emailExists = users.some((u) => u.id !== id && u.email.toLowerCase() === email.toLowerCase());
  if (emailExists) {
    return sendError(res, 409, 'CONFLICT', 'Ya existe otro usuario con ese email.', { field: 'email' });
  }

  const updated = { id, name, email };
  users[idx] = updated;

  res.status(200).json({ data: updated });
});

// PATCH /api/users/:id - actualización parcial
router.patch('/:id', (req, res) => {
  const id = toInt(req.params.id);
  if (id === null) {
    return sendError(res, 400, 'INVALID_ID', 'El parámetro id debe ser un entero.');
  }

  const idx = findUserIndexById(id);
  if (idx === -1) {
    return sendError(res, 404, 'NOT_FOUND', 'Usuario no encontrado.');
  }

  const current = users[idx];
  const patch = req.body || {};

  // Validación mínima: si viene name/email, validar formato
  let name = current.name;
  let email = current.email;

  if (patch.name !== undefined) {
    name = String(patch.name).trim();
    if (name.length < 2) {
      return sendError(res, 400, 'VALIDATION_ERROR', 'El nombre debe tener al menos 2 caracteres.', { field: 'name' });
    }
  }

  if (patch.email !== undefined) {
    email = String(patch.email).trim();
    if (!email.includes('@')) {
      return sendError(res, 400, 'VALIDATION_ERROR', 'Email inválido.', { field: 'email' });
    }

    const emailExists = users.some((u) => u.id !== id && u.email.toLowerCase() === email.toLowerCase());
    if (emailExists) {
      return sendError(res, 409, 'CONFLICT', 'Ya existe otro usuario con ese email.', { field: 'email' });
    }
  }

  const updated = { ...current, name, email };
  users[idx] = updated;

  res.status(200).json({ data: updated });
});

// DELETE /api/users/:id - eliminar
router.delete('/:id', (req, res) => {
  const id = toInt(req.params.id);
  if (id === null) {
    return sendError(res, 400, 'INVALID_ID', 'El parámetro id debe ser un entero.');
  }

  const idx = findUserIndexById(id);
  if (idx === -1) {
    return sendError(res, 404, 'NOT_FOUND', 'Usuario no encontrado.');
  }

  users.splice(idx, 1);

  // Opción A: 204 sin cuerpo (común en REST)
  res.status(204).send();
});

module.exports = router;

Códigos de estado: guía práctica para decidir rápido

  • 200 OK: lectura exitosa (GET) o actualización exitosa (PUT/PATCH) devolviendo el recurso.
  • 201 Created: creación exitosa (POST). Idealmente devuelve el recurso creado.
  • 204 No Content: eliminación exitosa (DELETE) sin cuerpo de respuesta.
  • 400 Bad Request: datos inválidos, faltan campos requeridos, formato incorrecto.
  • 404 Not Found: el recurso :id no existe.
  • 409 Conflict: conflicto con el estado actual (por ejemplo, email duplicado).

Errores consistentes: un formato simple y predecible

Si cada endpoint devuelve errores con el mismo “shape”, el frontend (o cualquier cliente) puede manejarlos sin casos especiales. En el ejemplo se usa:

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Faltan campos requeridos.",
    "details": { "missing": ["name", "email"] }
  }
}

Recomendaciones prácticas:

  • code: estable y orientado a máquina (para lógica del cliente).
  • message: legible para humanos.
  • details: opcional, útil para campos específicos o listas de errores.

Validaciones mínimas sin librerías pesadas

Antes de incorporar validadores externos, puedes cubrir lo esencial con funciones pequeñas:

  • Requeridos: comprobar undefined, null y cadena vacía.
  • Normalización: trim() en strings.
  • Formato básico: por ejemplo, email.includes('@') como validación mínima (no perfecta, pero útil para empezar).
  • Reglas de negocio: unicidad (email no repetido) con una búsqueda en el array.

Un patrón útil es validar y devolver el error lo antes posible (early return) para mantener el handler legible.

Probar el CRUD rápidamente con ejemplos de requests

Crear un usuario (POST)

POST /api/users
Content-Type: application/json

{
  "name": "Grace Hopper",
  "email": "grace@example.com"
}

Respuesta esperada:

201
{
  "data": { "id": 3, "name": "Grace Hopper", "email": "grace@example.com" }
}

Actualizar parcialmente (PATCH)

PATCH /api/users/3
Content-Type: application/json

{ "email": "grace.hopper@example.com" }

Respuesta esperada:

200
{
  "data": { "id": 3, "name": "Grace Hopper", "email": "grace.hopper@example.com" }
}

Error de validación (POST sin email)

POST /api/users
Content-Type: application/json

{ "name": "Sin Email" }

Respuesta esperada:

400
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Faltan campos requeridos.",
    "details": { "missing": ["email"] }
  }
}

Consejos de diseño para mantener la API consistente

  • Usa plural para colecciones: /api/users, /api/products.
  • Usa :id para un recurso: /api/users/:id.
  • No mezcles verbos en la URL: evita /api/users/create; usa POST /api/users.
  • Respuestas con envoltorio: { data: ... } para éxito y { error: ... } para fallos, de forma uniforme.
  • PUT vs PATCH: usa PUT cuando exiges el recurso completo; PATCH cuando permites actualizar solo algunos campos.

Ahora responde el ejercicio sobre el contenido:

Al diseñar rutas REST para usuarios en Express, ¿cuál opción aplica correctamente la diferencia entre PUT y PATCH sobre /api/users/:id?

¡Tienes razón! Felicitaciones, ahora pasa a la página siguiente.

¡Tú error! Inténtalo de nuevo.

En una API REST, PUT se usa para un reemplazo completo del recurso (requiere todos los campos), mientras PATCH permite una actualización parcial validando solo los campos presentes.

Siguiente capítulo

Middlewares en Express para lógica transversal

Arrow Right Icon
Portada de libro electrónico gratuitaNode.js para principiantes: crea un backend simple con Express
42%

Node.js para principiantes: crea un backend simple con Express

Nuevo curso

12 páginas

Descarga la aplicación para obtener una certificación gratuita y escuchar cursos en segundo plano, incluso con la pantalla apagada.