Detrás de cada aplicación móvil, cada sitio web moderno y cada integración entre sistemas hay una API. Son el puente invisible que conecta el frontend con el backend, las apps con los servidores y los sistemas entre sí.
Aprender a construir una es una de las habilidades más demandadas en el desarrollo web actual. Y con Node.js y Express, es sorprendentemente accesible.
En este tutorial vas a crear una API REST funcional y completa desde cero: con todas las operaciones CRUD, manejo de errores, estructura de proyecto profesional y cómo probarla. Sin saltar pasos.
¿Qué es una API REST? El concepto en 2 minutos
Antes de escribir una sola línea de código, necesitas entender qué estás construyendo.
Una API (Application Programming Interface) es un intermediario que permite que dos sistemas se comuniquen. Cuando abres una app del clima, la app le pregunta a una API: "¿qué temperatura hace en Lima?" La API consulta los datos y responde. Tú solo ves el número en pantalla.
Una API REST es un tipo específico de API que usa el protocolo HTTP y sigue unas reglas concretas:
- 🔗 Los recursos se identifican con URLs:
/usuarios,/productos/5,/pedidos - ⚡ Las acciones se expresan con verbos HTTP:
| Verbo HTTP | Acción | Ejemplo |
|---|---|---|
GET | Leer / Obtener | Traer todos los productos |
POST | Crear | Agregar un nuevo producto |
PUT | Actualizar completo | Reemplazar un producto |
PATCH | Actualizar parcial | Cambiar solo el precio |
DELETE | Eliminar | Borrar un producto |
- 📦 Las respuestas viajan en formato JSON — el lenguaje universal de las APIs
- 🔢 Cada respuesta incluye un código de estado HTTP: 200 (OK), 201 (Creado), 404 (No encontrado), 500 (Error del servidor)
Lo que vamos a construir: una API REST para gestionar una lista de productos de una tienda. Tendrá todas las operaciones CRUD y seguirá las convenciones del mundo real.
¿Qué es Node.js y por qué usarlo para APIs?
Node.js es un entorno de ejecución que permite usar JavaScript fuera del navegador, en el servidor. Esto significa que puedes usar el mismo lenguaje tanto en el frontend (React, Vue) como en el backend (Node.js). Un solo lenguaje para todo: eso es muy poderoso.
Express.js es el framework web más popular para Node.js. Es minimalista, flexible y tiene un ecosistema enorme. La gran mayoría de las APIs en el mundo JavaScript se construyen con Express.
Paso 1: Instala Node.js
Antes de empezar, verifica si ya tienes Node.js instalado:
node --version
npm --versionSi ves números de versión (algo como v20.x.x), perfecto. Si no, descarga Node.js desde nodejs.org e instala la versión LTS (Long Term Support). NPM viene incluido con Node.js, no necesitas instalarlo por separado.
💡 En 2026, la versión LTS recomendada es Node.js 22.x.
Paso 2: Crea el proyecto
Abre la terminal y ejecuta estos comandos:
# Crea la carpeta del proyecto
mkdir api-productos
cd api-productos
# Inicializa el proyecto Node.js
npm init -yEl comando npm init -y crea el archivo package.json, que es el "pasaporte" de tu proyecto: contiene el nombre, la versión y las dependencias.
Instala las dependencias
npm install express cors- express — el framework web
- cors — permite que otros dominios (tu frontend) puedan hacer peticiones a tu API
Y una herramienta de desarrollo que recarga el servidor automáticamente cuando cambias el código:
npm install --save-dev nodemonAbre el package.json y agrega estos scripts:
{
"scripts": {
"start": "node src/index.js",
"dev": "nodemon src/index.js"
}
}Estructura del proyecto
Crea esta estructura de carpetas:
api-productos/
├── src/
│ ├── index.js ← punto de entrada
│ ├── app.js ← configuración de Express
│ ├── routes/
│ │ └── productos.js ← rutas de productos
│ └── data/
│ └── productos.js ← datos en memoria (sin base de datos)
├── package.json
└── .gitignoreCrea el archivo .gitignore con este contenido:
node_modules/
.envPaso 3: Configura Express
Primero, crea los datos de ejemplo. En src/data/productos.js:
// src/data/productos.js
// En una app real, esto vendría de una base de datos
// Por ahora usamos datos en memoria para enfocarnos en la API
let productos = [
{
id: 1,
nombre: "Laptop Pro 15",
precio: 899.99,
stock: 20,
categoria: "tecnologia"
},
{
id: 2,
nombre: "Mouse inalámbrico",
precio: 29.99,
stock: 150,
categoria: "tecnologia"
},
{
id: 3,
nombre: "Teclado mecánico",
precio: 79.99,
stock: 80,
categoria: "tecnologia"
},
{
id: 4,
nombre: "Silla ergonómica",
precio: 299.99,
stock: 15,
categoria: "muebles"
}
];
module.exports = productos;Ahora configura Express en src/app.js:
// src/app.js
const express = require('express');
const cors = require('cors');
const productosRoutes = require('./routes/productos');
const app = express();
// ============================================
// MIDDLEWARES GLOBALES
// ============================================
// Permite recibir JSON en el body de las peticiones
app.use(express.json());
// Permite peticiones desde otros dominios (tu frontend)
app.use(cors());
// ============================================
// RUTAS
// ============================================
// Ruta de verificación — útil para saber si la API está activa
app.get('/', (req, res) => {
res.json({
mensaje: '🚀 API de Productos funcionando correctamente',
version: '1.0.0',
endpoints: {
productos: '/api/productos'
}
});
});
// Rutas de productos
app.use('/api/productos', productosRoutes);
// ============================================
// MANEJO DE RUTAS NO ENCONTRADAS
// ============================================
app.use('*', (req, res) => {
res.status(404).json({
error: 'Ruta no encontrada',
ruta: req.originalUrl
});
});
// ============================================
// MANEJO GLOBAL DE ERRORES
// ============================================
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({
error: 'Error interno del servidor',
mensaje: err.message
});
});
module.exports = app;Y el punto de entrada en src/index.js:
// src/index.js
const app = require('./app');
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`✅ Servidor corriendo en http://localhost:${PORT}`);
console.log(`📋 Endpoints disponibles:`);
console.log(` GET http://localhost:${PORT}/api/productos`);
console.log(` GET http://localhost:${PORT}/api/productos/:id`);
console.log(` POST http://localhost:${PORT}/api/productos`);
console.log(` PUT http://localhost:${PORT}/api/productos/:id`);
console.log(` DELETE http://localhost:${PORT}/api/productos/:id`);
});Paso 4: Crea las rutas CRUD
Aquí está el corazón de la API. En src/routes/productos.js:
// src/routes/productos.js
const express = require('express');
const router = express.Router();
const productos = require('../data/productos');
// ============================================
// GET /api/productos — Obtener todos
// ============================================
router.get('/', (req, res) => {
const { categoria, precioMax } = req.query;
let resultado = [...productos];
// Filtrar por categoría si se especifica
if (categoria) {
resultado = resultado.filter(p =>
p.categoria.toLowerCase() === categoria.toLowerCase()
);
}
// Filtrar por precio máximo si se especifica
if (precioMax) {
resultado = resultado.filter(p => p.precio <= Number(precioMax));
}
res.status(200).json({
total: resultado.length,
productos: resultado
});
});
// ============================================
// GET /api/productos/:id — Obtener uno
// ============================================
router.get('/:id', (req, res) => {
const id = parseInt(req.params.id);
const producto = productos.find(p => p.id === id);
if (!producto) {
return res.status(404).json({
error: `Producto con id ${id} no encontrado`
});
}
res.status(200).json(producto);
});
// ============================================
// POST /api/productos — Crear uno nuevo
// ============================================
router.post('/', (req, res) => {
const { nombre, precio, stock, categoria } = req.body;
// Validación básica
if (!nombre || !precio || !categoria) {
return res.status(400).json({
error: 'Los campos nombre, precio y categoría son obligatorios'
});
}
if (typeof precio !== 'number' || precio <= 0) {
return res.status(400).json({
error: 'El precio debe ser un número positivo'
});
}
// Crear el nuevo producto
const nuevoProducto = {
id: productos.length + 1,
nombre: nombre.trim(),
precio: Number(precio.toFixed(2)),
stock: stock || 0,
categoria: categoria.toLowerCase().trim()
};
productos.push(nuevoProducto);
res.status(201).json({
mensaje: 'Producto creado exitosamente',
producto: nuevoProducto
});
});
// ============================================
// PUT /api/productos/:id — Actualizar completo
// ============================================
router.put('/:id', (req, res) => {
const id = parseInt(req.params.id);
const index = productos.findIndex(p => p.id === id);
if (index === -1) {
return res.status(404).json({
error: `Producto con id ${id} no encontrado`
});
}
const { nombre, precio, stock, categoria } = req.body;
if (!nombre || !precio || !categoria) {
return res.status(400).json({
error: 'Los campos nombre, precio y categoría son obligatorios'
});
}
// Reemplazar el producto completo
productos[index] = {
id,
nombre: nombre.trim(),
precio: Number(precio.toFixed(2)),
stock: stock ?? productos[index].stock,
categoria: categoria.toLowerCase().trim()
};
res.status(200).json({
mensaje: 'Producto actualizado exitosamente',
producto: productos[index]
});
});
// ============================================
// PATCH /api/productos/:id — Actualizar parcial
// ============================================
router.patch('/:id', (req, res) => {
const id = parseInt(req.params.id);
const index = productos.findIndex(p => p.id === id);
if (index === -1) {
return res.status(404).json({
error: `Producto con id ${id} no encontrado`
});
}
// Actualizar solo los campos enviados
const camposPermitidos = ['nombre', 'precio', 'stock', 'categoria'];
const actualizaciones = {};
for (const campo of camposPermitidos) {
if (req.body[campo] !== undefined) {
actualizaciones[campo] = req.body[campo];
}
}
productos[index] = { ...productos[index], ...actualizaciones };
res.status(200).json({
mensaje: 'Producto actualizado parcialmente',
producto: productos[index]
});
});
// ============================================
// DELETE /api/productos/:id — Eliminar
// ============================================
router.delete('/:id', (req, res) => {
const id = parseInt(req.params.id);
const index = productos.findIndex(p => p.id === id);
if (index === -1) {
return res.status(404).json({
error: `Producto con id ${id} no encontrado`
});
}
const productoEliminado = productos.splice(index, 1)[0];
res.status(200).json({
mensaje: 'Producto eliminado exitosamente',
producto: productoEliminado
});
});
module.exports = router;Paso 5: Arranca el servidor
Ejecuta este comando en la terminal:
npm run devDeberías ver:
✅ Servidor corriendo en http://localhost:3000
📋 Endpoints disponibles:
GET http://localhost:3000/api/productos
GET http://localhost:3000/api/productos/:id
POST http://localhost:3000/api/productos
PUT http://localhost:3000/api/productos/:id
DELETE http://localhost:3000/api/productos/:idAbre el navegador y ve a http://localhost:3000. Verás la respuesta JSON de tu API. 🎉
Paso 6: Prueba tu API
Puedes probar la API de varias formas. Aquí van las principales:
Opción A: Desde el navegador (solo GET)
Abre estas URLs directamente:
http://localhost:3000/api/productoshttp://localhost:3000/api/productos/1http://localhost:3000/api/productos?categoria=tecnologiahttp://localhost:3000/api/productos?precioMax=100
Opción B: Con Thunder Client en VS Code (recomendada)
Instala la extensión Thunder Client en VS Code. Es gratuita y te permite hacer peticiones de todos los tipos (GET, POST, PUT, DELETE) sin salir del editor.
Opción C: Con curl desde la terminal
# GET todos los productos
curl http://localhost:3000/api/productos
# GET un producto por ID
curl http://localhost:3000/api/productos/1
# POST crear un producto
curl -X POST http://localhost:3000/api/productos \
-H "Content-Type: application/json" \
-d '{"nombre": "Monitor 4K", "precio": 399.99, "stock": 10, "categoria": "tecnologia"}'
# PUT actualizar un producto completo
curl -X PUT http://localhost:3000/api/productos/1 \
-H "Content-Type: application/json" \
-d '{"nombre": "Laptop Pro 16", "precio": 999.99, "stock": 15, "categoria": "tecnologia"}'
# PATCH actualizar solo el precio
curl -X PATCH http://localhost:3000/api/productos/1 \
-H "Content-Type: application/json" \
-d '{"precio": 849.99}'
# DELETE eliminar un producto
curl -X DELETE http://localhost:3000/api/productos/2Respuestas que deberías ver
Al crear un producto (POST):
{
"mensaje": "Producto creado exitosamente",
"producto": {
"id": 5,
"nombre": "Monitor 4K",
"precio": 399.99,
"stock": 10,
"categoria": "tecnologia"
}
}Al intentar obtener un producto que no existe (GET /api/productos/99):
{
"error": "Producto con id 99 no encontrado"
}Paso 7: Entiende los middlewares
Un middleware es una función que se ejecuta entre que llega la petición y que se envía la respuesta. Es como un guardia de seguridad que inspecciona, modifica o rechaza peticiones antes de que lleguen a tu lógica de negocio.
// Así funciona el flujo:
// Petición → Middleware 1 → Middleware 2 → Ruta → Respuesta
// express.json() es un middleware que transforma el body de la
// petición de texto raw a un objeto JavaScript accesible como req.body
app.use(express.json());
// Puedes crear tus propios middlewares:
const registrarPeticion = (req, res, next) => {
const ahora = new Date().toISOString();
console.log(`[${ahora}] ${req.method} ${req.url}`);
next(); // ← IMPORTANTE: llama al siguiente middleware o ruta
};
app.use(registrarPeticion);
// Ahora cada petición quedará registrada en consola:
// [2026-03-29T14:35:00.000Z] GET /api/productos
// [2026-03-29T14:35:05.000Z] POST /api/productosPaso 8: Agrega variables de entorno
Los valores como el puerto o las credenciales de base de datos nunca deben estar hardcodeados en el código. Para eso existen las variables de entorno:
npm install dotenvCrea un archivo .env en la raíz del proyecto:
# .env — nunca subas este archivo a GitHub
PORT=3000
NODE_ENV=developmentY cárgalas al inicio de src/index.js:
// src/index.js
require('dotenv').config(); // ← primera línea, antes de todo
const app = require('./app');
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`✅ Servidor en modo ${process.env.NODE_ENV}`);
console.log(`🚀 http://localhost:${PORT}`);
});Estructura final del proyecto
api-productos/
├── src/
│ ├── index.js ← punto de entrada, arranca el servidor
│ ├── app.js ← configuración de Express y middlewares
│ ├── routes/
│ │ └── productos.js ← todas las rutas de /api/productos
│ └── data/
│ └── productos.js ← datos en memoria
├── .env ← variables de entorno (no subir a GitHub)
├── .gitignore
└── package.jsonEn proyectos más grandes, también encontrarás carpetas como controllers/, models/, middlewares/ y services/, pero esta estructura es perfecta para empezar.
¿Y la base de datos? El siguiente paso natural
Esta API guarda los datos en memoria, lo que significa que se pierden al reiniciar el servidor. En el mundo real, conectarías tu API a una base de datos.
Las opciones más comunes con Node.js:
- 🐬 MySQL / PostgreSQL con la librería
mysql2o el ORMSequelize— para datos relacionales - 🍃 MongoDB con la librería
mongoose— para datos en documentos JSON - 🪶 SQLite con
better-sqlite3— para proyectos pequeños o de aprendizaje
El flujo de conexión siempre es el mismo: instalar la librería, configurar la conexión y reemplazar el array en memoria por consultas reales a la base de datos.
Errores comunes y cómo evitarlos
Olvidar el middleware express.json()
// Sin esto, req.body siempre será undefined en POST y PUT
app.use(express.json()); // ← obligatorio para recibir JSONNo manejar el caso de "no encontrado"
// ❌ Malo: si el producto no existe, la respuesta es undefined
router.get('/:id', (req, res) => {
const producto = productos.find(p => p.id === parseInt(req.params.id));
res.json(producto); // undefined si no existe
});
// ✅ Bueno: verificar siempre si existe antes de responder
router.get('/:id', (req, res) => {
const producto = productos.find(p => p.id === parseInt(req.params.id));
if (!producto) return res.status(404).json({ error: 'No encontrado' });
res.json(producto);
});No validar los datos que entran
Nunca confíes en los datos que llegan en el body. Valida siempre que los campos obligatorios existen y tienen el tipo correcto antes de procesarlos.
Olvidar los códigos de estado correctos
200— OK (GET, PUT, PATCH, DELETE exitoso)201— Creado (POST exitoso)400— Bad Request (datos inválidos del cliente)404— Not Found (recurso no encontrado)500— Internal Server Error (error del servidor)
Resumen: lo que construiste hoy
- ✅ Entendiste qué es una API REST y cómo funciona
- ✅ Configuraste un proyecto Node.js con Express desde cero
- ✅ Organizaste el proyecto con una estructura profesional
- ✅ Creaste endpoints para todas las operaciones CRUD
- ✅ Implementaste filtros con query parameters
- ✅ Manejaste errores con los códigos de estado HTTP correctos
- ✅ Usaste middlewares para procesar JSON y registrar peticiones
- ✅ Configuraste variables de entorno con dotenv
- ✅ Probaste la API desde el navegador, curl y Thunder Client
🧪 ¿Ya tienes claros los fundamentos de JavaScript para avanzar?
Node.js usa JavaScript. Si hay conceptos del lenguaje que todavía no tienes del todo claros — callbacks, promesas, arrow functions, desestructuración — este es el momento perfecto para reforzarlos antes de profundizar en backend.
👉 Test: Fundamentos de JavaScript 👉 Test: APIs REST Diseño y Consumo
¿Lograste arrancar el servidor y probar todos los endpoints? ¿Tuviste algún error en el camino? Cuéntanos en los comentarios 👇 — respondemos todos. 🚀
No hay comentarios todavía. Sé el primero en compartir tu opinión.