Guía Completa de API REST con Express.js

Desarrollo profesional de APIs con Node.js, Express, Sequelize y JWT

Introducción

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.

¿Qué aprenderás?

1. Configuración del Proyecto

Comencemos inicializando nuestro proyecto e instalando las dependencias necesarias:

Terminal
# 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

Estructura del Proyecto

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

Beneficios de esta estructura

2. Configuración Básica de Express

Vamos a configurar nuestra aplicación Express con los middlewares esenciales:

app.js
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;

Explicación de Middlewares Clave

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

3. Configuración de Sequelize ORM

Sequelize es un ORM para Node.js que nos permite trabajar con bases de datos SQL de manera sencilla:

config/db.js
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;

Ventajas de usar 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

4. Modelado de Datos con Sequelize

Definamos nuestro modelo de Producto con validaciones y relaciones:

models/Producto.js
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;

Características Avanzadas Implementadas

5. Implementación de Controladores

Los controladores manejan la lógica de negocio de nuestra API:

controllers/productos.js
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)

Patrones Implementados

6. Documentación Automática con Swagger

Implementación de OpenAPI/Swagger para documentación interactiva:

utils/swagger.js
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'
    }));
};
routes/productos.js (fragmento documentado)
/**
 * @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);

Acceso a la Documentación

Una vez implementado, la documentación interactiva estará disponible en:

GET http://tuservidor.com/api-docs

Vista previa de Swagger UI

7. Despliegue en Producción

Configuración recomendada para entornos productivos:

config/config.env
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

Mejores Prácticas para Producción

Scripts recomendados en package.json

package.json
"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"
}