CRUD completo para sistema de inventario con autenticación JWT y documentación Swagger
Este manual te guiará en la creación de una API RESTful completa para gestión de inventario utilizando:
# Inicializar proyecto
npm init -y
# Instalar dependencias principales
npm install express mongoose cors jsonwebtoken bcryptjs dotenv
# Dependencias para desarrollo
npm install --save-dev nodemon swagger-jsdoc swagger-ui-express
/
├── config/
│ └── db.js # Conexión a MongoDB
├── models/
│ └── Product.js # Modelo de Mongoose
├── routes/
│ ├── auth.js # Rutas de autenticación
│ └── products.js # Rutas de productos
├── controllers/
│ ├── auth.js # Lógica de autenticación
│ └── products.js # Lógica de productos
├── middlewares/
│ ├── auth.js # Middleware de JWT
│ └── error.js # Manejo de errores
├── utils/
│ └── apiFeatures.js # Funciones auxiliares
├── app.js # Configuración de Express
└── server.js # Punto de entrada
const mongoose = require('mongoose');
require('dotenv').config();
const connectDB = async () => {
try {
await mongoose.connect(process.env.MONGO_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
useCreateIndex: true,
useFindAndModify: false
});
console.log('MongoDB Connected...');
} catch (err) {
console.error('Database connection error:', err.message);
process.exit(1);
}
};
module.exports = connectDB;
MONGO_URI=mongodb+srv://usuario:password@cluster0.mongodb.net/inventario?retryWrites=true&w=majority
JWT_SECRET=miSuperSecreto123
JWT_EXPIRE=30d
useNewUrlParser
y useUnifiedTopology
const mongoose = require('mongoose');
const validator = require('validator');
const productSchema = new mongoose.Schema({
name: {
type: String,
required: [true, 'El nombre es obligatorio'],
trim: true,
maxlength: [100, 'El nombre no puede exceder 100 caracteres'],
unique: true
},
description: {
type: String,
maxlength: [500, 'La descripción no puede exceder 500 caracteres']
},
price: {
type: Number,
required: [true, 'El precio es obligatorio'],
min: [0, 'El precio no puede ser negativo'],
set: val => Math.round(val * 100) / 100 // 2 decimales
},
stock: {
type: Number,
default: 0,
min: [0, 'El stock no puede ser negativo']
},
category: {
type: String,
enum: {
values: ['electronics', 'clothing', 'food', 'other'],
message: 'Categoría no válida'
}
},
createdAt: {
type: Date,
default: Date.now
}
}, {
toJSON: { virtuals: true },
toObject: { virtuals: true }
});
// Índice para búsquedas por nombre y categoría
productSchema.index({ name: 1, category: 1 });
// Middleware para hooks
productSchema.pre('save', function(next) {
console.log(`Guardando producto: ${this.name}`);
next();
});
const Product = mongoose.model('Product', productSchema);
module.exports = Product;
const Product = require('../models/Product');
const APIFeatures = require('../utils/apiFeatures');
const AppError = require('../utils/appError');
// @desc Obtener todos los productos
// @route GET /api/v1/products
// @access Public
exports.getProducts = async (req, res, next) => {
try {
const features = new APIFeatures(Product.find(), req.query)
.filter()
.sort()
.limitFields()
.paginate();
const products = await features.query;
res.status(200).json({
success: true,
count: products.length,
data: products
});
} catch (err) {
next(err);
}
};
// @desc Crear nuevo producto
// @route POST /api/v1/products
// @access Private/Admin
exports.createProduct = async (req, res, next) => {
try {
const product = await Product.create(req.body);
res.status(201).json({
success: true,
data: product
});
} catch (err) {
next(err);
}
};
// @desc Actualizar producto
// @route PUT /api/v1/products/:id
// @access Private/Admin
exports.updateProduct = async (req, res, next) => {
try {
const product = await Product.findByIdAndUpdate(
req.params.id,
req.body,
{
new: true,
runValidators: true
}
);
if (!product) {
return next(new AppError('Producto no encontrado', 404));
}
res.status(200).json({
success: true,
data: product
});
} catch (err) {
next(err);
}
};
// @desc Eliminar producto
// @route DELETE /api/v1/products/:id
// @access Private/Admin
exports.deleteProduct = async (req, res, next) => {
try {
const product = await Product.findByIdAndDelete(req.params.id);
if (!product) {
return next(new AppError('Producto no encontrado', 404));
}
res.status(200).json({
success: true,
data: {}
});
} catch (err) {
next(err);
}
};
Clase auxiliar para filtrado, ordenación, paginación y selección de campos:
class APIFeatures {
constructor(query, queryStr) {
this.query = query;
this.queryStr = queryStr;
}
filter() {
const queryObj = { ...this.queryStr };
const excludedFields = ['page', 'sort', 'limit', 'fields'];
excludedFields.forEach(el => delete queryObj[el]);
let queryStr = JSON.stringify(queryObj);
queryStr = queryStr.replace(/\b(gte|gt|lte|lt)\b/g, match => `$${match}`);
this.query = this.query.find(JSON.parse(queryStr));
return this;
}
sort() {
if (this.queryStr.sort) {
const sortBy = this.queryStr.sort.split(',').join(' ');
this.query = this.query.sort(sortBy);
} else {
this.query = this.query.sort('-createdAt');
}
return this;
}
limitFields() {
if (this.queryStr.fields) {
const fields = this.queryStr.fields.split(',').join(' ');
this.query = this.query.select(fields);
} else {
this.query = this.query.select('-__v');
}
return this;
}
paginate() {
const page = this.queryStr.page * 1 || 1;
const limit = this.queryStr.limit * 1 || 100;
const skip = (page - 1) * limit;
this.query = this.query.skip(skip).limit(limit);
return this;
}
}
const jwt = require('jsonwebtoken');
const User = require('../models/User');
const AppError = require('../utils/appError');
// Proteger rutas
exports.protect = async (req, res, next) => {
let token;
if (req.headers.authorization && req.headers.authorization.startsWith('Bearer')) {
token = req.headers.authorization.split(' ')[1];
}
if (!token) {
return next(new AppError('No autorizado - Inicie sesión', 401));
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = await User.findById(decoded.id);
next();
} catch (err) {
return next(new AppError('Token inválido', 401));
}
};
// Restringir a ciertos roles
exports.restrictTo = (...roles) => {
return (req, res, next) => {
if (!roles.includes(req.user.role)) {
return next(new AppError('No tiene permisos para esta acción', 403));
}
next();
};
};
const jwt = require('jsonwebtoken');
const User = require('../models/User');
const AppError = require('../utils/appError');
// @desc Iniciar sesión
// @route POST /api/v1/auth/login
// @access Public
exports.login = async (req, res, next) => {
const { email, password } = req.body;
if (!email || !password) {
return next(new AppError('Por favor ingrese email y contraseña', 400));
}
const user = await User.findOne({ email }).select('+password');
if (!user || !(await user.comparePassword(password))) {
return next(new AppError('Credenciales incorrectas', 401));
}
const token = jwt.sign({ id: user._id }, process.env.JWT_SECRET, {
expiresIn: process.env.JWT_EXPIRE
});
res.status(200).json({
success: true,
token
});
};
const swaggerJsdoc = require('swagger-jsdoc');
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',
},
servers: [
{ url: 'http://localhost:3000/api/v1' }
],
components: {
securitySchemes: {
bearerAuth: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT'
}
},
schemas: {
Product: {
type: 'object',
required: ['name', 'price'],
properties: {
name: { type: 'string', example: 'Laptop HP' },
price: { type: 'number', example: 1299.99 },
stock: { type: 'integer', example: 15 }
}
}
}
}
},
apis: ['./routes/*.js']
};
const specs = swaggerJsdoc(options);
module.exports = specs;
const swaggerUi = require('swagger-ui-express');
const swaggerSpec = require('./utils/swagger');
// Documentación
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec));
/**
* @swagger
* /products:
* get:
* summary: Obtener todos los productos
* tags: [Products]
* parameters:
* - in: query
* name: category
* schema:
* type: string
* description: Filtrar por categoría
* responses:
* 200:
* description: Lista de productos
* content:
* application/json:
* schema:
* type: array
* items:
* $ref: '#/components/schemas/Product'
*/
router.get('/', productController.getProducts);
const express = require('express');
const cors = require('cors');
const morgan = require('morgan');
const connectDB = require('./config/db');
const errorHandler = require('./middlewares/error');
// Importar rutas
const productRoutes = require('./routes/products');
const authRoutes = require('./routes/auth');
// Conectar a MongoDB
connectDB();
// Inicializar Express
const app = express();
// Middlewares
app.use(cors());
app.use(express.json());
// Logger en desarrollo
if (process.env.NODE_ENV === 'development') {
app.use(morgan('dev'));
}
// Montar rutas
app.use('/api/v1/products', productRoutes);
app.use('/api/v1/auth', authRoutes);
// Manejo de errores
app.use(errorHandler);
module.exports = app;
const app = require('./app');
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Servidor corriendo en puerto ${PORT} en modo ${process.env.NODE_ENV}`);
});
Obtener todos los productos (filtros, ordenación, paginación)
Crear nuevo producto (requiere autenticación JWT)
Actualizar producto (requiere autenticación JWT)
Eliminar producto (requiere autenticación JWT y rol admin)
Autenticación de usuario (obtener token JWT)
Documentación interactiva de la API