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ón | Método | Ruta típica | Resultado esperado |
|---|---|---|---|
| Listar | GET | /api/users | Devuelve colección |
| Obtener uno | GET | /api/users/:id | Devuelve un recurso |
| Crear | POST | /api/users | Crea y devuelve el recurso |
| Actualizar completo | PUT | /api/users/:id | Reemplaza el recurso |
| Actualizar parcial | PATCH | /api/users/:id | Modifica campos |
| Eliminar | DELETE | /api/users/:id | Elimina 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:
- Escuche el audio con la pantalla apagada.
- Obtenga un certificado al finalizar.
- ¡Más de 5000 cursos para que explores!
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
:idno 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,nully 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
:idpara un recurso:/api/users/:id. - No mezcles verbos en la URL: evita
/api/users/create; usaPOST /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.