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);
});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'
});
});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);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.jsImplementació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'));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:
| Orden | Tipo de Ruta | Ejemplo | Notas |
|---|---|---|---|
| 1 | Ruta exacta | /users/admin | Se matching antes que /users/:id |
| 2 | Ruta con regex | /items/:id([0-9]+) | Validación inline |
| 3 | Parámetro estándar | /users/:id | Catch-all para IDs |
| 4 | Wildcard | /* | Último recurso |
/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ón | Uso | Beneficio |
|---|---|---|
express.Router() | Modularizar rutas | Organización y mantenibilidad |
| Middleware encadenados | Validación, auth, logging | Separación de responsabilidades |
| Route parameters | Recursos anidados | URLs semánticas |
| Query strings | Filtrado, paginación | Flexibilidad sin cambiar estructura |
| app.route() | Encadenar métodos HTTP | Código más limpio |
| mergeParams: true | Rutas anidadas | Acceso a parámetros padre |
¿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
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()
¿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