Desarrollo profesional de APIs con Node.js, Express, Sequelize y JWT
Este manual cubre el desarrollo completo de una API RESTful para gestión de inventario utilizando las mejores tecnologías y prácticas del ecosistema Node.js.
Comencemos inicializando nuestro proyecto e instalando las dependencias necesarias:
# Inicializar proyecto Node.js
npm init -y
# Instalar dependencias principales
npm install express sequelize mysql2 cors jsonwebtoken
# Dependencias para documentación
npm install swagger-jsdoc swagger-ui-express
# Herramientas de desarrollo
npm install --save-dev dotenv nodemon
Organización modular profesional:
/
├── config/ # Configuraciones
│ └── db.js # Conexión a base de datos
├── controllers/ # Lógica de negocio
│ └── productos.js # Controlador de productos
├── models/ # Modelos de datos
│ └── Producto.js # Modelo Sequelize
├── routes/ # Definición de rutas
│ └── productos.js # Rutas de productos
├── middlewares/ # Middlewares personalizados
│ ├── auth.js # Autenticación JWT
│ └── error.js # Manejo de errores
├── utils/ # Utilidades
│ └── apiFeatures.js # Funciones auxiliares
├── app.js # Aplicación principal
└── server.js # Punto de entrada
Vamos a configurar nuestra aplicación Express con los middlewares esenciales:
const express = require('express');
const cors = require('cors');
const morgan = require('morgan');
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const AppError = require('./utils/appError');
const app = express();
// 1) Middlewares globales
app.use(cors()); // Habilitar CORS
app.use(helmet()); // Seguridad HTTP
app.use(express.json({ limit: '10kb' })); // Parsear JSON
// Logger de desarrollo
if (process.env.NODE_ENV === 'development') {
app.use(morgan('dev'));
}
// Limitar peticiones
const limiter = rateLimit({
max: 100,
windowMs: 60 * 60 * 1000,
message: 'Demasiadas peticiones desde esta IP, intenta nuevamente en una hora'
});
app.use('/api', limiter);
// 2) Rutas
app.use('/api/v1/productos', require('./routes/productos'));
// 3) Manejo de rutas no encontradas
app.all('*', (req, res, next) => {
next(new AppError(`No se encontró ${req.originalUrl} en este servidor`, 404));
});
// 4) Middleware de manejo de errores
app.use(require('./middlewares/error'));
module.exports = app;
Middleware | Propósito |
---|---|
cors() |
Permite solicitudes entre dominios (Cross-Origin Resource Sharing) |
helmet() |
Protege la app configurando cabeceras HTTP seguras |
express.json() |
Parsea el cuerpo de las solicitudes en formato JSON |
morgan |
Logger HTTP para desarrollo (registra peticiones) |
rateLimit |
Previene ataques de fuerza bruta limitando peticiones |
Sequelize es un ORM para Node.js que nos permite trabajar con bases de datos SQL de manera sencilla:
const { Sequelize } = require('sequelize');
const config = require('../config');
const sequelize = new Sequelize(
config.db.name,
config.db.user,
config.db.password,
{
host: config.db.host,
dialect: 'mysql',
logging: config.env === 'development' ? console.log : false,
pool: {
max: 5,
min: 0,
acquire: 30000,
idle: 10000
},
define: {
timestamps: true,
underscored: true,
paranoid: true
}
}
);
// Autenticación y sincronización
sequelize.authenticate()
.then(() => {
console.log('Conexión a la base de datos establecida');
if (config.env === 'development') {
sequelize.sync({ alter: true });
}
})
.catch(err => {
console.error('Error al conectar a la base de datos:', err);
process.exit(1);
});
module.exports = sequelize;
Característica | Beneficio |
---|---|
Modelado de datos | Sintaxis clara para definir modelos y relaciones |
Independencia de DB | Mismo código para MySQL, PostgreSQL, SQLite, etc. |
Migrations | Control de versiones para esquema de base de datos |
Query Builder | Métodos intuitivos para consultas complejas |
Validaciones | Validación a nivel de modelo antes de guardar |
Definamos nuestro modelo de Producto con validaciones y relaciones:
const { DataTypes } = require('sequelize');
const sequelize = require('../config/db');
const Categoria = require('./Categoria');
const Producto = sequelize.define('Producto', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
nombre: {
type: DataTypes.STRING(100),
allowNull: false,
validate: {
notNull: { msg: 'El nombre es requerido' },
len: {
args: [3, 100],
msg: 'El nombre debe tener entre 3 y 100 caracteres'
}
}
},
descripcion: {
type: DataTypes.TEXT,
validate: {
len: {
args: [0, 500],
msg: 'La descripción no puede exceder 500 caracteres'
}
}
},
precio: {
type: DataTypes.DECIMAL(10, 2),
allowNull: false,
validate: {
isDecimal: { msg: 'El precio debe ser un número decimal' },
min: { args: [0], msg: 'El precio no puede ser negativo' }
}
},
stock: {
type: DataTypes.INTEGER,
defaultValue: 0,
validate: {
isInt: { msg: 'El stock debe ser un número entero' }
}
},
estado: {
type: DataTypes.ENUM('activo', 'inactivo', 'agotado'),
defaultValue: 'activo'
}
}, {
paranoid: true, // Borrado lógico
indexes: [
{
unique: true,
fields: ['nombre']
},
{
fields: ['precio']
}
]
});
// Relación Muchos-a-Uno con Categoría
Producto.belongsTo(Categoria, {
foreignKey: {
allowNull: false
},
onDelete: 'RESTRICT'
});
module.exports = Producto;
paranoid: true
para no eliminar físicamente registrosLos controladores manejan la lógica de negocio de nuestra API:
const { Producto } = require('../models');
const APIFeatures = require('../utils/apiFeatures');
const AppError = require('../utils/appError');
exports.getAllProductos = async (req, res, next) => {
try {
// 1) Filtrar, ordenar, paginar
const features = new APIFeatures(Producto, req.query)
.filter()
.sort()
.limitFields()
.paginate();
// 2) Ejecutar consulta
const productos = await features.query;
res.status(200).json({
status: 'success',
results: productos.length,
data: { productos }
});
} catch (err) {
next(err);
}
};
exports.getProducto = async (req, res, next) => {
try {
const producto = await Producto.findByPk(req.params.id);
if (!producto) {
return next(new AppError('No se encontró el producto con ese ID', 404));
}
res.status(200).json({
status: 'success',
data: { producto }
});
} catch (err) {
next(err);
}
};
exports.createProducto = async (req, res, next) => {
try {
const producto = await Producto.create(req.body);
res.status(201).json({
status: 'success',
data: { producto }
});
} catch (err) {
next(err);
}
};
// ... (métodos update y delete similares)
APIFeatures
para consultas avanzadasImplementación de OpenAPI/Swagger para documentación interactiva:
const swaggerJsdoc = require('swagger-jsdoc');
const swaggerUi = require('swagger-ui-express');
const options = {
definition: {
openapi: '3.0.0',
info: {
title: 'API de Inventario',
version: '1.0.0',
description: 'Documentación para la API de gestión de inventario',
contact: {
name: 'Equipo de Desarrollo',
email: 'dev@inventario.com'
},
license: {
name: 'MIT'
}
},
servers: [
{ url: 'http://localhost:3000/api/v1', description: 'Servidor de desarrollo' },
{ url: 'https://api.inventario.com/v1', description: 'Servidor de producción' }
],
components: {
securitySchemes: {
bearerAuth: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT'
}
},
schemas: {
Producto: {
type: 'object',
required: ['nombre', 'precio'],
properties: {
id: { type: 'integer', example: 1 },
nombre: { type: 'string', example: 'Laptop HP' },
precio: { type: 'number', format: 'float', example: 1299.99 },
stock: { type: 'integer', example: 15 }
}
}
}
}
},
apis: ['./routes/*.js', './models/*.js']
};
const specs = swaggerJsdoc(options);
module.exports = (app) => {
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(specs, {
explorer: true,
customSiteTitle: 'API Inventario Docs'
}));
};
/**
* @swagger
* tags:
* name: Productos
* description: Gestión de productos del inventario
*/
/**
* @swagger
* /productos:
* get:
* summary: Obtener todos los productos
* tags: [Productos]
* parameters:
* - in: query
* name: page
* schema:
* type: integer
* description: Número de página
* - in: query
* name: limit
* schema:
* type: integer
* description: Límite de resultados por página
* responses:
* 200:
* description: Lista de productos
* content:
* application/json:
* schema:
* type: array
* items:
* $ref: '#/components/schemas/Producto'
*/
router.get('/', productoController.getAllProductos);
Una vez implementado, la documentación interactiva estará disponible en:
GET http://tuservidor.com/api-docs
Configuración recomendada para entornos productivos:
NODE_ENV=production
PORT=3000
# Base de datos
DB_HOST=cluster-db.inventario.com
DB_USER=prod_user
DB_PASSWORD=segura123
DB_NAME=inventario_prod
# JWT
JWT_SECRET=miSuperSecretoComplejo123!
JWT_EXPIRES_IN=90d
JWT_COOKIE_EXPIRES=90
# Email (para recuperación de contraseña)
EMAIL_USERNAME=notificaciones@inventario.com
EMAIL_PASSWORD=emailpass123
EMAIL_HOST=smtp.sendgrid.net
EMAIL_PORT=587
"scripts": {
"start": "NODE_ENV=production node server.js",
"dev": "nodemon server.js",
"test": "jest --watchAll",
"lint": "eslint .",
"migrate": "sequelize-cli db:migrate",
"seed": "sequelize-cli db:seed:all"
}