Construir una API REST es una de las habilidades más demandadas en desarrollo web. En este tutorial vamos a crear una API completa desde cero con Node.js y Express — incluyendo CRUD, validación, manejo de errores, middleware de autenticación y estructura profesional de proyecto. No solo el código funcional, sino la arquitectura que uso en proyectos reales.
¿Qué vamos a construir?
Una API REST para gestionar una lista de tareas (todo-list) con las siguientes funcionalidades:
- CRUD completo (Crear, Leer, Actualizar, Eliminar)
- Validación de datos de entrada
- Manejo centralizado de errores
- Middleware de autenticación con API Key
- Estructura de carpetas profesional
- Variables de entorno con dotenv
El código fuente completo está disponible para que lo descargues y lo ejecutes. Vamos paso a paso.
Requisitos previos
Necesitas tener instalado:
- Node.js versión 18 o superior (recomiendo la versión LTS)
- Un editor de código — VS Code es ideal con la extensión REST Client o Thunder Client
- Terminal / línea de comandos
Verifica tu instalación:
node --version # v20.x.x o superior
npm --version # 10.x.x o superior
Paso 1: Inicializar el proyecto
# Crear directorio del proyecto
mkdir todo-api && cd todo-api
# Inicializar package.json
npm init -y
# Instalar dependencias
npm install express dotenv uuid
# Instalar dependencias de desarrollo
npm install -D nodemon
Actualiza el package.json para agregar scripts útiles:
{
"name": "todo-api",
"version": "1.0.0",
"scripts": {
"start": "node src/index.js",
"dev": "nodemon src/index.js"
}
}
Paso 2: Estructura de carpetas
Esta es la estructura que uso en mis proyectos Node.js. Separar responsabilidades desde el inicio te ahorra dolores de cabeza cuando el proyecto crece:
todo-api/
├── src/
│ ├── index.js # Punto de entrada
│ ├── app.js # Configuración de Express
│ ├── routes/
│ │ └── tareas.js # Rutas de tareas
│ ├── controllers/
│ │ └── tareasController.js # Lógica de negocio
│ ├── middleware/
│ │ ├── auth.js # Autenticación
│ │ ├── errorHandler.js # Manejo de errores
│ │ └── validator.js # Validación
│ └── data/
│ └── store.js # Almacenamiento en memoria
├── .env
├── .gitignore
└── package.json
Paso 3: Configuración base de Express
Crea el archivo .env en la raíz del proyecto:
PORT=3000
API_KEY=mi-clave-secreta-2026
NODE_ENV=development
El archivo src/app.js — configuración central de Express:
const express = require('express');
const tareasRoutes = require('./routes/tareas');
const errorHandler = require('./middleware/errorHandler');
const app = express();
// Middleware globales
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Ruta de health check
app.get('/api/health', (req, res) => {
res.json({
status: 'ok',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
});
});
// Rutas de la API
app.use('/api/tareas', tareasRoutes);
// Ruta 404 para endpoints no encontrados
app.use('*', (req, res) => {
res.status(404).json({
error: 'Endpoint no encontrado',
metodo: req.method,
ruta: req.originalUrl,
});
});
// Middleware de manejo de errores (siempre al final)
app.use(errorHandler);
module.exports = app;
El archivo src/index.js — punto de entrada:
require('dotenv').config();
const app = require('./app');
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`API corriendo en http://localhost:${PORT}`);
console.log(`Entorno: ${process.env.NODE_ENV}`);
console.log(`Health check: http://localhost:${PORT}/api/health`);
});
Paso 4: Almacenamiento de datos
Para este tutorial usamos almacenamiento en memoria. En un proyecto real, aquí conectarías MongoDB, PostgreSQL u otra base de datos. La interfaz es la misma:
Archivo src/data/store.js:
const { v4: uuidv4 } = require('uuid');
// "Base de datos" en memoria
let tareas = [
{
id: uuidv4(),
titulo: 'Aprender Node.js',
descripcion: 'Completar el tutorial de API REST',
completada: false,
prioridad: 'alta',
creadaEn: new Date().toISOString(),
actualizadaEn: new Date().toISOString(),
},
];
const store = {
getAll: () => tareas,
getById: (id) => tareas.find((t) => t.id === id),
create: (data) => {
const nueva = {
id: uuidv4(),
...data,
completada: false,
creadaEn: new Date().toISOString(),
actualizadaEn: new Date().toISOString(),
};
tareas.push(nueva);
return nueva;
},
update: (id, data) => {
const index = tareas.findIndex((t) => t.id === id);
if (index === -1) return null;
tareas[index] = {
...tareas[index],
...data,
actualizadaEn: new Date().toISOString(),
};
return tareas[index];
},
delete: (id) => {
const index = tareas.findIndex((t) => t.id === id);
if (index === -1) return false;
tareas.splice(index, 1);
return true;
},
};
module.exports = store;
Paso 5: Controlador con lógica de negocio
Archivo src/controllers/tareasController.js:
const store = require('../data/store');
const tareasController = {
// GET /api/tareas
listar: (req, res) => {
let tareas = store.getAll();
// Filtro por estado
if (req.query.completada !== undefined) {
const completada = req.query.completada === 'true';
tareas = tareas.filter((t) => t.completada === completada);
}
// Filtro por prioridad
if (req.query.prioridad) {
tareas = tareas.filter((t) => t.prioridad === req.query.prioridad);
}
res.json({
total: tareas.length,
datos: tareas,
});
},
// GET /api/tareas/:id
obtener: (req, res) => {
const tarea = store.getById(req.params.id);
if (!tarea) {
return res.status(404).json({ error: 'Tarea no encontrada' });
}
res.json(tarea);
},
// POST /api/tareas
crear: (req, res) => {
const nueva = store.create({
titulo: req.body.titulo,
descripcion: req.body.descripcion || '',
prioridad: req.body.prioridad || 'media',
});
res.status(201).json(nueva);
},
// PUT /api/tareas/:id
actualizar: (req, res) => {
const actualizada = store.update(req.params.id, req.body);
if (!actualizada) {
return res.status(404).json({ error: 'Tarea no encontrada' });
}
res.json(actualizada);
},
// DELETE /api/tareas/:id
eliminar: (req, res) => {
const eliminada = store.delete(req.params.id);
if (!eliminada) {
return res.status(404).json({ error: 'Tarea no encontrada' });
}
res.json({ mensaje: 'Tarea eliminada correctamente' });
},
};
module.exports = tareasController;
Paso 6: Middleware de validación
Nunca confíes en los datos que llegan del cliente. Este middleware valida los datos antes de que lleguen al controlador.
Archivo src/middleware/validator.js:
const validarTarea = (req, res, next) => {
const { titulo, prioridad } = req.body;
const errores = [];
// Validar título (requerido)
if (!titulo || typeof titulo !== 'string') {
errores.push('El título es obligatorio y debe ser texto');
} else if (titulo.trim().length < 3) {
errores.push('El título debe tener al menos 3 caracteres');
} else if (titulo.trim().length > 200) {
errores.push('El título no puede exceder 200 caracteres');
}
// Validar prioridad (opcional, pero si viene debe ser válida)
const prioridadesValidas = ['baja', 'media', 'alta'];
if (prioridad && !prioridadesValidas.includes(prioridad)) {
errores.push(`La prioridad debe ser: ${prioridadesValidas.join(', ')}`);
}
if (errores.length > 0) {
return res.status(400).json({ errores });
}
// Sanitizar datos
req.body.titulo = titulo.trim();
next();
};
module.exports = { validarTarea };
Paso 7: Middleware de autenticación
Protegemos las rutas de escritura (POST, PUT, DELETE) con una API Key simple. En un proyecto real usarías JWT o OAuth 2.0.
Archivo src/middleware/auth.js:
const autenticar = (req, res, next) => {
const apiKey = req.headers['x-api-key'];
if (!apiKey) {
return res.status(401).json({
error: 'API Key requerida',
detalle: 'Incluye el header X-API-KEY en tu petición',
});
}
if (apiKey !== process.env.API_KEY) {
return res.status(403).json({
error: 'API Key inválida',
});
}
next();
};
module.exports = autenticar;
Paso 8: Manejo centralizado de errores
En lugar de manejar errores en cada ruta, usamos un middleware central que captura todos los errores no manejados:
Archivo src/middleware/errorHandler.js:
const errorHandler = (err, req, res, next) => {
console.error(`[ERROR] ${req.method} ${req.path}:`, err.message);
// Error de JSON malformado
if (err.type === 'entity.parse.failed') {
return res.status(400).json({
error: 'JSON inválido en el body de la petición',
});
}
// Error genérico del servidor
res.status(err.status || 500).json({
error: process.env.NODE_ENV === 'production'
? 'Error interno del servidor'
: err.message,
});
};
module.exports = errorHandler;
Paso 9: Definir las rutas
Archivo src/routes/tareas.js:
const express = require('express');
const router = express.Router();
const tareasController = require('../controllers/tareasController');
const autenticar = require('../middleware/auth');
const { validarTarea } = require('../middleware/validator');
// Rutas públicas (solo lectura)
router.get('/', tareasController.listar);
router.get('/:id', tareasController.obtener);
// Rutas protegidas (escritura - requieren API Key)
router.post('/', autenticar, validarTarea, tareasController.crear);
router.put('/:id', autenticar, validarTarea, tareasController.actualizar);
router.delete('/:id', autenticar, tareasController.eliminar);
module.exports = router;
Paso 10: Probar la API
Inicia el servidor en modo desarrollo:
npm run dev
Ahora prueba cada endpoint con curl desde otra terminal:
# Health check
curl http://localhost:3000/api/health
# Listar todas las tareas
curl http://localhost:3000/api/tareas
# Obtener una tarea por ID (reemplaza el ID)
curl http://localhost:3000/api/tareas/TU-UUID-AQUI
# Crear una tarea (requiere API Key)
curl -X POST http://localhost:3000/api/tareas \
-H "Content-Type: application/json" \
-H "X-API-KEY: mi-clave-secreta-2026" \
-d '{"titulo": "Estudiar Express", "prioridad": "alta"}'
# Actualizar una tarea
curl -X PUT http://localhost:3000/api/tareas/TU-UUID-AQUI \
-H "Content-Type: application/json" \
-H "X-API-KEY: mi-clave-secreta-2026" \
-d '{"titulo": "Estudiar Express (completado)", "completada": true}'
# Eliminar una tarea
curl -X DELETE http://localhost:3000/api/tareas/TU-UUID-AQUI \
-H "X-API-KEY: mi-clave-secreta-2026"
# Filtrar tareas pendientes
curl "http://localhost:3000/api/tareas?completada=false"
# Filtrar por prioridad
curl "http://localhost:3000/api/tareas?prioridad=alta"
# Probar error de validación
curl -X POST http://localhost:3000/api/tareas \
-H "Content-Type: application/json" \
-H "X-API-KEY: mi-clave-secreta-2026" \
-d '{"titulo": "ab"}'
# Probar error de autenticación
curl -X POST http://localhost:3000/api/tareas \
-H "Content-Type: application/json" \
-d '{"titulo": "Sin API Key"}'
Códigos de estado HTTP más usados en APIs
Es importante devolver el código correcto según la especificación HTTP:
| Código | Significado | Cuándo usarlo |
|---|---|---|
200 | OK | Lectura o actualización exitosa |
201 | Created | Recurso creado exitosamente |
400 | Bad Request | Datos de entrada inválidos |
401 | Unauthorized | Falta autenticación |
403 | Forbidden | Credenciales inválidas |
404 | Not Found | Recurso no existe |
500 | Internal Server Error | Error inesperado del servidor |
Buenas prácticas para APIs REST
- Usa sustantivos en plural para las rutas:
/api/tareas(no/api/getTareaso/api/tarea). - Versiona tu API: Cuando tu API crezca, usa versionado:
/api/v1/tareas,/api/v2/tareas. - Valida siempre la entrada: Nunca confíes en datos del cliente. Para proyectos más grandes, usa Joi o Zod para validación de esquemas.
- Respuestas consistentes: Mantén un formato uniforme — siempre devuelve JSON con la misma estructura de éxito/error.
- Logging: En producción usa Winston o Pino en lugar de
console.log. - CORS: Si tu frontend está en otro dominio, instala
npm install corsy configúralo en Express. - Rate Limiting: Protege tu API de abuso con express-rate-limit.
- Documentación: Usa Swagger/OpenAPI para documentar tus endpoints automáticamente.
Próximos pasos
Este tutorial cubre los fundamentos de una API REST profesional. Para llevarlo al siguiente nivel:
- Base de datos real: Reemplaza el store en memoria con Mongoose (MongoDB) o Prisma (PostgreSQL/MySQL).
- Autenticación JWT: Implementa registro, login y tokens JWT con jsonwebtoken.
- Testing: Escribe tests con Jest y Supertest para cada endpoint.
- Docker: Contenedoriza tu API para deployments consistentes.
- Deploy: Despliega en Railway, Render o un VPS con Nginx como reverse proxy.
La clave para dominar el desarrollo backend es la práctica. Toma este proyecto base, agrégale funcionalidades nuevas (paginación, búsqueda, uploads) y construye algo propio. Cada problema que resuelvas te acerca a ser un mejor desarrollador.