Sistema de routing avanzado

Lectura
30 min~8 min lectura
CONCEPTO CLAVE: El sistema de routing en Express es el núcleo de cualquier API REST. Dominar el routing avanzado te permitirá crear aplicaciones escalables, mantenibles y bien organizadas, separando claramente las responsabilidades de cada módulo y facilitando el desarrollo colaborativo.

Introducción al Routing Avanzado en Express

En lecciones anteriores cubrimos los fundamentos del routing básico. Ahora profundizaremos en técnicas avanzadas que te permitirán manejar casos de uso complejos, organizar tu código en módulos reutilizables y optimizar el rendimiento de tu API REST.

Express proporciona un sistema de routing extremadamente flexible que va mucho más allá de simplemente definir rutas y manejadores. Vamos a explorar características avanzadas que te distinguirán como desarrollador profesional.

Parámetros de Ruta y Query Strings

El routing dinámico es esencial para cualquier API REST profesional. Express ofrece dos tipos principales de parámetros:

Parámetros de Ruta (Route Parameters)

Los parámetros de ruta se definen con dos puntos (:paramName) y capturan segmentos variables de la URL:

// app.js
const express = require('express');
const app = express();

// Parámetro simple
app.get('/users/:userId', (req, res) => {
  const { userId } = req.params;
  res.json({ userId, message: `Usuario ${userId}` });
});

// Múltiples parámetros
app.get('/users/:userId/posts/:postId', (req, res) => {
  const { userId, postId } = req.params;
  res.json({ userId, postId });
});

// Parámetros con formato específico usando expresión regular
app.get('/items/:id([0-9]+)', (req, res) => {
  res.json({ id: req.params.id, type: 'numeric' });
});

// Parámetros opcionales (usando regex)
app.get('/docs/:section?/:topic?', (req, res) => {
  res.json(req.params);
});
💡 Tip Profesional: Usa expresiones regulares en tus parámetros para validar el formato desde la ruta. Esto previene que solicitudes inválidas lleguen a tus manejadores y reduce código de validación.

Query Strings (Parámetros de Consulta)

Los query strings se accede a través de req.query y son ideales para filtrado, paginación y ordenamiento:

// GET /products?category=electronics&minPrice=100&maxPrice=500&sort=price
app.get('/products', (req, res) => {
  const { category, minPrice, maxPrice, sort, page = 1, limit = 10 } = req.query;
  
  // Lógica de filtrado y paginación
  const filters = {};
  if (category) filters.category = category;
  if (minPrice) filters.minPrice = parseFloat(minPrice);
  if (maxPrice) filters.maxPrice = parseFloat(maxPrice);
  
  const skip = (parseInt(page) - 1) * parseInt(limit);
  
  res.json({
    filters,
    pagination: { page: parseInt(page), limit: parseInt(limit), skip },
    sort: sort || 'createdAt'
  });
});
📌 Buena Práctica: Define valores por defecto para query strings para evitar undefined. Además, siempre valida y sanitiza los valores recibidos para prevenir inyecciones y errores inesperados.

Manejadores de Ruta Múltiples (Route Handlers)

Una de las características más poderosas de Express es poder encadenar múltiples manejadores para una misma ruta. Esto permite separar préoccupations y crear middleware reutilizable.

// Middleware de autenticación
const authenticate = (req, res, next) => {
  const token = req.headers.authorization?.split(' ')[1];
  if (!token) {
    return res.status(401).json({ error: 'Token requerido' });
  }
  // Validar token...
  req.user = { id: 1, role: 'admin' };
  next();
};

// Middleware de validación
const validateRequest = (req, res, next) => {
  const { title, content } = req.body;
  if (!title || title.length < 3) {
    return res.status(400).json({ error: 'Título inválido' });
  }
  next();
};

// Manejador principal
const createPost = (req, res) => {
  const post = { ...req.body, authorId: req.user.id };
  res.status(201).json(post);
};

// Encadenar manejadores
app.post('/posts', authenticate, validateRequest, createPost);

// Usando array de middleware (más limpio para múltiples handlers)
const postMiddleware = [
  authenticate,
  validateRequest,
  (req, res, next) => {
    // Logging personalizado
    console.log(`Creando post para usuario ${req.user.id}`);
    next();
  }
];

app.post('/posts', postMiddleware, createPost);
⚠️ Importante: Siempre llama a next() en tus middleware. Si olvidas llamarlo, la solicitud quedará colgada y eventualmente generará un timeout. Usa next('route') para saltar al siguiente manejador de ruta.

Organización con express.Router()

Para APIs grandes, usar express.Router() es fundamental. Permite crear módulos de rutas independientes y montarlos en la aplicación principal.

Estructura de Proyecto Recomendada

/project
├── app.js              # Aplicación principal
├── /routes
│   ├── index.js        # Rutas base
│   ├── users.js        # Rutas de usuarios
│   ├── products.js     # Rutas de productos
│   └── orders.js       # Rutas de pedidos
├── /middleware
│   ├── auth.js
│   ├── validation.js
│   └── errorHandler.js
└── /controllers
    ├── userController.js
    └── productController.js

Implementación de Routers Modularizados

// routes/users.js
const express = require('express');
const router = express.Router();
const userController = require('../controllers/userController');
const { authenticate } = require('../middleware/auth');
const { validateUser } = require('../middleware/validation');

// Todas las rutas aquí tienen prefijo /users

// GET /users
router.get('/', userController.getAll);

// GET /users/:id
router.get('/:id', userController.getById);

// POST /users
router.post('/', validateUser, userController.create);

// PUT /users/:id
router.put('/:id', authenticate, validateUser, userController.update);

// DELETE /users/:id
router.delete('/:id', authenticate, userController.delete);

module.exports = router;
// routes/products.js
const express = require('express');
const router = express.Router({ mergeParams: true }); // Permite acceder a params del padre
const productController = require('../controllers/productController');

// Rutas específicas de productos por usuario
// GET /users/:userId/products
router.get('/', productController.getByUser);

// Nested routes: GET /users/:userId/products/:productId
router.get('/:productId', productController.getUserProduct);

module.exports = router;
// app.js
const express = require('express');
const app = express();

// Importar routers
const usersRouter = require('./routes/users');
const productsRouter = require('./routes/products');
const ordersRouter = require('./routes/orders');

// Middleware global
app.use(express.json());
app.use((req, res, next) => {
  console.log(`${req.method} ${req.path}`);
  next();
});

// Montar routers
app.use('/users', usersRouter);
app.use('/users/:userId/products', productsRouter); // Rutas anidadas
app.use('/orders', ordersRouter);

// Ruta raíz
app.get('/', (req, res) => {
  res.json({ message: 'API REST con Express', version: '2.0' });
});

// Manejo de errores 404
app.use((req, res) => {
  res.status(404).json({ error: 'Ruta no encontrada' });
});

app.listen(3000, () => console.log('Servidor en puerto 3000'));
💡 Tip: Usa la opción mergeParams: true en express.Router() cuando necesites acceder a parámetros de rutas padre, como :userId en rutas anidadas.

Métodos HTTP Especiales

app.route() - Chain de Métodos

El método app.route() permite encadenar diferentes métodos HTTP para una misma ruta, mejorando la legibilidad:

app.route('/articles')
  .get((req, res) => {
    // Listar artículos
    res.json({ articles: [] });
  })
  .post((req, res) => {
    // Crear artículo
    res.status(201).json({ created: true });
  });

app.route('/articles/:id')
  .get((req, res) => {
    res.json({ id: req.params.id });
  })
  .put((req, res) => {
    res.json({ updated: true });
  })
  .patch((req, res) => {
    res.json({ patched: true });
  })
  .delete((req, res) => {
    res.status(204).send();
  });

app.all() - Manejador Universal

app.all() se ejecuta para todos los métodos HTTP, útil para logging o validación común:

// Aplicar a todas las rutas /api/*
app.all('/api/*', (req, res, next) => {
  res.header('Access-Control-Allow-Origin', '*');
  res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH');
  res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
  
  if (req.method === 'OPTIONS') {
    return res.sendStatus(200);
  }
  next();
});

Prioridad de Rutas y Route Matching

Express evalúa las rutas en orden de definición. Las rutas estáticas tienen prioridad sobre las dinámicas:

OrdenTipo de RutaEjemploNotas
1Ruta exacta/users/adminSe matching antes que /users/:id
2Ruta con regex/items/:id([0-9]+)Validación inline
3Parámetro estándar/users/:idCatch-all para IDs
4Wildcard/*Último recurso
⚠️ Cuidado: Define siempre las rutas específicas ANTES de las genéricas. Si colocas /users/:id antes de /users/admin, nunca podrás acceder a la ruta del administrador.

Técnicas Avanzadas de Routing

Router con Prefijo de Versión API

// versioning-router.js
const express = require('express');
const router = express.Router();

// Aplicar versionado a todas las rutas
router.use('/v1', require('./routes/v1'));
router.use('/v2', require('./routes/v2'));

module.exports = router;

// app.js
app.use('/api', require('./routes/versioning-router'));

Rutas con Prefijo Dinámico

// Middleware para tenant isolation
const tenantRouter = express.Router();

tenantRouter.use((req, res, next) => {
  const tenantId = req.headers['x-tenant-id'];
  if (!tenantId) {
    return res.status(400).json({ error: 'Tenant ID requerido' });
  }
  req.tenantId = tenantId;
  next();
});

// Todas las rutas ahora están aisladas por tenant
tenantRouter.get('/data', (req, res) => {
  res.json({ tenant: req.tenantId, data: [] });
});
Ver más: Ejemplo Completo de API REST Modular
// Estructura completa de una API REST profesional

// routes/index.js
const express = require('express');
const router = express.Router();

// Importar todos los routers
router.use('/auth', require('./auth'));
router.use('/users', require('./users'));
router.use('/products', require('./products'));
router.use('/orders', require('./orders'));
router.use('/analytics', require('./analytics'));

// Health check
router.get('/health', (req, res) => {
  res.json({ status: 'healthy', timestamp: new Date().toISOString() });
});

module.exports = router;

// app.js - punto de entrada
const express = require('express');
const app = express();
const routes = require('./routes');
const errorHandler = require('./middleware/errorHandler');

app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true }));

// Rate limiting básico
app.use('/api', require('./middleware/rateLimiter'));

// Montar API
app.use('/api', routes);

// Error handler global
app.use(errorHandler);

module.exports = app;
La modularización del routing no es solo una buena práctica, es una necesidad para proyectos que crecerán. Invierte tiempo en структурировать tu código desde el principio y te ahorrará refactorizaciones dolorosas en el futuro.

Resumen de Patrones de Routing Avanzado

PatrónUsoBeneficio
express.Router()Modularizar rutasOrganización y mantenibilidad
Middleware encadenadosValidación, auth, loggingSeparación de responsabilidades
Route parametersRecursos anidadosURLs semánticas
Query stringsFiltrado, paginaciónFlexibilidad sin cambiar estructura
app.route()Encadenar métodos HTTPCódigo más limpio
mergeParams: trueRutas anidadasAcceso a parámetros padre
📌 Recuerda: El routing avanzado es sobre arquitectura y mantenibilidad. Un buen sistema de routing hace que tu API sea intuitiva para otros desarrolladores y fácil de extender sin romper funcionalidades existentes.
🧠 Quiz: Sistema de Routing Avanzado

¿Cuál es el propósito principal de usar express.Router() en lugar de definir todas las rutas directamente en app.js?

  • A) Mejorar el rendimiento de la aplicación
  • B) Modularizar y organizar el código en componentes reutilizables
  • C) Reducir el uso de memoria del servidor
  • D) Habilitar conexiones websocket automáticamente
✅ Respuesta correcta: B) Modularizar y organizar el código en componentes reutilizables. Express.Router() permite crear módulos de rutas independientes que pueden montarse en diferentes puntos de la aplicación, facilitando el mantenimiento y la colaboración en equipos.
🧠 Quiz: Route Parameters

Si necesitas validar que un parámetro de ruta sea exclusivamente numérico, ¿cuál método usarías?

  • A) req.params.type = 'number'
  • B) router.validate(':id', 'numeric')
  • C) app.get('/items/:id([0-9]+)')
  • D) params.check(':id').isNumeric()
✅ Respuesta correcta: C) app.get('/items/:id([0-9]+)') usando expresiones regulares inline. Esta es la forma nativa de Express para validar el formato de los parámetros directamente en la definición de la ruta.
🧠 Quiz: Middleware en Rutas

¿Qué debes hacer siempre en un middleware de Express (excepto el último manejador de la cadena)?

  • A) Llamar a res.send()
  • B) Llamar a next()
  • C) Retornar un valor
  • D) Usar async/await
✅ Respuesta correcta: B) Llamar a next(). Si olvidas llamar a next(), la solicitud quedará colgada. Puedes usar next('route') para saltar al siguiente manejador de ruta o next(err) para pasar al manejador de errores.
💡 Próximos Pasos: Practica creando una API REST completa con múltiples routers, autenticación por middleware y validación de parámetros. Implementa un sistema de versionado API y observa cómo mejora la organización de tu código.