Crear una herramienta de automatización modular en JavaScript desde cero
Aprenda cómo combinar Playwright, Cheerio, SQLite y Commander en un motor de flujo de trabajo reutilizable para Node.js que evoluciona de un script a un producto de automatización vendible.
1. Un problema recurrente se convirtió en el punto de partida
En algún momento te das cuenta de que estás realizando una y otra vez el mismo tipo de tarea.
Cargas una página.
La escaneas en busca de algo útil.
Sacas las partes relevantes.
Las guardas en algún lugar para más tarde.
Pasas a la siguiente página.
Comienzas el ciclo de nuevo.
Ninguno de estos pasos es difícil por sí solo, pero la repetición constante resulta absurda.
Así que, en lugar de adoptar otro framework de JavaScript solo para crear otra interfaz de control, un enfoque más útil es desarrollar algo que realmente puedas utilizar: una herramienta de automatización y recolección de datos en JavaScript.
La idea detrás de esto es sencilla:
Entrega una tarea al programa → deja que JavaScript se encargue de la parte repetitiva → obtén resultados estructurados.
Una versión inicial de una herramienta así solo necesita un puñado de tecnologías:
- Node.js
- Playwright
- Cheerio
- SQLite
- Commander
Esa combinación es suficiente para convertir un pequeño script en algo que comienza a parecerse a un producto real.
2. Playwright como primer bloque de construcción
El objetivo era permitir que JavaScript controlara un navegador real.
Playwright hace eso mucho más fácil de lo esperado.
const { chromium } = require("playwright");
async function visitWebsite(url) {
const browser = await chromium.launch({
headless: false
});
const page = await browser.newPage();
await page.goto(url, {
waitUntil: "domcontentloaded"
});
console.log(
"Page title:",
await page.title()
);
console.log(
"Current URL:",
page.url()
);
await browser.close();
}
visitWebsite(
"https://example.com"
);
La primera vez que ejecutas algo así, parece casi demasiado sencillo.
JavaScript abre un navegador.
JavaScript navega a una página.
JavaScript lee su contenido.
JavaScript cierra el navegador.
Solo con eso ya tienes una base sobre la cual puedes desarrollar pruebas de navegadores, monitoreo, flujos de trabajo repetitivos y automatización en general.
3. De las coordenadas a los elementos
Algo que hay que evitar desde el principio es la automatización frágil.
Al decirle a su programa algo como:
Haga clic en esta ubicación exacta.
eso significa que la automatización puede fallar en cuanto el diseño cambie, aunque sea ligeramente.
Un enfoque mejor es escribir código que describa con qué interactúa realmente el usuario, en lugar de indicar dónde se encuentran las cosas en la pantalla.
async function searchPage(page, query) {
await page
.getByRole("textbox")
.fill(query);
await page
.getByRole("button", {
name: "Search"
})
.click();
await page.waitForLoadState(
"domcontentloaded"
);
}
Esto es mucho más fácil de mantener con el tiempo.
El código no dice:
Haga clic en lo que esté en las coordenadas 742, 381.
Sino que dice:
Encuentre el cuadro de texto y el botón de búsqueda.
Se trata de una pequeña elección de diseño, pero hace que la automatización en los navegadores sea drásticamente menos problemática con el paso del tiempo.
4. Extracción de datos de la página
Una vez que el navegador puede navegar por una página por sí mismo, el siguiente paso es hacer que recopile información.
Imagínese, por ejemplo, una página llena de tarjetas de productos.
async function extractProducts(page) {
return page
.locator(".product-card")
.evaluateAll(cards => {
return cards.map(card => {
const name =
card
.querySelector(".product-name")
?.textContent
?.trim();
const price =
card
.querySelector(".price")
?.textContent
?.trim();
return {
name,
price
};
});
});
}
En este punto el navegador ya no se limita a visitar páginas.
Convierte lo que hay en la página en objetos de JavaScript.
Eso hace que los datos sean utilizables para un procesamiento posterior.
const products =
await extractProducts(page);
console.log(
JSON.stringify(
products,
null,
2
)
);
Una vez que la información tiene una estructura real, se puede almacenar, comparar, analizar o pasarla a otra parte de la aplicación.
5. Utilizando Cheerio para el análisis de HTML
Playwright brilla cuando realmente se necesita que un navegador esté en ejecución.
Pero a menudo ya se tiene el HTML listo y no es necesario iniciar Chromium en absoluto.
Ahí es donde Cheerio demuestra su utilidad.
const cheerio = require("cheerio");
function parseProducts(html) {
const $ = cheerio.load(html);
const products = [];
$(".product-card").each(
(_, element) => {
const name = $(element)
.find(".product-name")
.text()
.trim();
const price = $(element)
.find(".price")
.text()
.trim();
products.push({
name,
price
});
}
);
return products;
}
Tener ambos herramientas a mano es una verdadera ventaja.
Playwright se encarga de la interacción con el navegador.
Cheerio se ocupa del análisis ligero de HTML.
De esta manera, solo se utiliza una instancia completa del navegador cuando es realmente necesario, y un analizador simple cubre el resto.
6. Dotar a la automatización de memoria con SQLite
El almacenamiento fue el siguiente desafío por resolver.
Si un script recopila datos hoy, ¿dónde estarán esos datos mañana?
La solución fue utilizar SQLite.
const sqlite3 = require("sqlite3").verbose();
const db = new sqlite3.Database(
"automation.db"
);
db.run(`
CREATE TABLE IF NOT EXISTS products (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
price TEXT,
source TEXT,
created_at DATETIME
DEFAULT CURRENT_TIMESTAMP
)
`);
A partir de ahí, una función dedicada se encargó de guardar cada resultado.
function saveProduct(product, source) {
return new Promise(
(resolve, reject) => {
db.run(
`
INSERT INTO products
(name, price, source)
VALUES (?, ?, ?)
`,
[
product.name,
product.price,
source
],
error => {
if (error) {
reject(error);
return;
}
resolve();
}
);
}
);
}
Esta adición cambió la estructura del proyecto de manera significativa.
Con una base de datos en su lugar, los registros históricos podían acumularse con el tiempo. Eso abrió la posibilidad de responder a preguntas prácticas como:
- ¿Qué cambió?
- ¿Qué apareció recientemente?
- ¿Qué desapareció?
La automatización se vuelve mucho más valiosa una vez que puede recordar lo que sucedió anteriormente.
7. Creación de un motor de flujos de trabajo reutilizable
En esta etapa, el proyecto contaba con varias partes independientes: control del navegador, análisis de HTML y persistencia en la base de datos. Lo que faltaba era una estructura; existía un riesgo real de terminar con funciones sueltas dispersas por todas partes en lugar de un sistema coherente.
Para solucionarlo, una clase de flujo de trabajo unió todo en conjunto.
class AutomationWorkflow {
constructor(browser) {
this.browser = browser;
this.page = null;
}
async start() {
this.page =
await this.browser.newPage();
}
async visit(url) {
await this.page.goto(url, {
waitUntil: "domcontentloaded"
});
}
async search(query) {
await this.page
.getByRole("textbox")
.fill(query);
await this.page
.getByRole("button", {
name: "Search"
})
.click();
}
async getTitle() {
return this.page.title();
}
async close() {
await this.page.close();
}
}
Con esa clase en su lugar, es sencillo seguir un tarea completa de principio a fin.
const browser =
await chromium.launch({
headless: false
});
const workflow =
new AutomationWorkflow(
browser
);
await workflow.start();
await workflow.visit(
"https://example.com"
);
await workflow.search(
"JavaScript automation"
);
console.log(
await workflow.getTitle()
);
await workflow.close();
await browser.close();
Este es el punto en el que el diseño orientado a objetos comenzó a dar sus frutos. La instancia de la clase modela el flujo de trabajo en sí, y sus métodos representan las operaciones individuales. El resto del código no necesita saber cómo se implementa internamente cada paso.
8. Manejo de errores con lógica de reintentos
Los scripts de automatización suelen funcionar sin problemas durante nueve intentos y luego fallar en el décimo. La red se ralentiza, una página no se carga completamente, un servidor tiene problemas o un elemento tarda más de lo esperado en renderizarse.
Para abordar esto, se introdujo una función auxiliar genérica de reintentos.
async function retry(
operation,
attempts = 3,
delay = 2000
) {
let lastError;
for (
let attempt = 1;
attempt <= attempts;
attempt++
) {
try {
return await operation();
} catch (error) {
lastError = error;
console.log(
`Attempt ${attempt} failed.`
);
if (
attempt < attempts
) {
await new Promise(
resolve =>
setTimeout(
resolve,
delay
)
);
}
}
}
throw lastError;
}
Esa herramienta permitió encapsular los pasos críticos con reintentos automáticos.
await retry(
async () => {
await page.goto(
"https://example.com",
{
waitUntil:
"domcontentloaded"
}
);
},
3,
1500
);
La conclusión fue clara: la automatización de nivel profesional debe planificar las fallas como un evento normal, no como una excepción. Las tutoriales suelen dar por sentado que la red es perfectamente cooperativa. Los sistemas reales no pueden permitirse hacer esa suposición.
9. Envolverlo en una interfaz de línea de comandos
En algún momento, abrir el archivo fuente cada vez solo para cambiar una URL se volvió tedioso.
El objetivo pasó a hacer que la herramienta se comportara como una utilidad de línea de comandos adecuada.
Commander facilitó mucho lograrlo.
const { Command } = require("commander");
const program = new Command();
program
.name("webpilot")
.description(
"JavaScript automation toolkit"
)
.version("1.0.0");
program
.command("visit")
.description(
"Open a webpage"
)
.argument("<url>")
.action(async url => {
const browser =
await chromium.launch({
headless: false
});
const page =
await browser.newPage();
await page.goto(url);
console.log(
await page.title()
);
await browser.close();
});
program.parseAsync();
Con eso en su lugar, la herramienta podía iniciarse directamente desde una terminal.
node webpilot.js visit https://example.com
Parece un ajuste menor, pero cambia fundamentalmente la forma en que interactúas con el software.
En lugar de editar el programa cada vez que necesitas algo,
simplemente lo ejecutas.
10. Tratar a la IA como la interfaz de usuario
Eso planteó una nueva pregunta:
¿Por qué debería el usuario memorizar la sintaxis exacta de los comandos?
En lugar de escribir algo como:
node webpilot.js screenshot https://example.com
una persona podría simplemente describir la intención en lenguaje sencillo:
"Tome una captura de pantalla de la página principal."
Una capa de IA podría traducir esa oración en un objeto de tarea estructurado.
const task = {
action: "screenshot",
url: "https://example.com",
output: "homepage.png"
};
No obstante, a la IA nunca se le dará permiso para ejecutar JavaScript arbitrario directamente.
Cada acción solicitada pasará primero por un paso de validación.
const allowedActions = new Set([
"visit",
"search",
"screenshot",
"download"
]);
function validateTask(task) {
if (
!allowedActions.has(
task.action
)
) {
throw new Error(
"Unsupported action."
);
}
if (
task.url &&
!task.url.startsWith("https://")
) {
throw new Error(
"Invalid URL."
);
}
return true;
}
Esto permite una separación clara de responsabilidades:
La IA interpreta lo que quiere el usuario.
La capa de JavaScript decide qué está permitido realmente.
Playwright solo lleva a cabo la acción aprobada.
Ese tipo de diseño en capas es un enfoque mucho más seguro que otorgar a un modelo de IA un control directo e ilimitado sobre la máquina.
11. Reenmarcar el proyecto como un producto vendible
En esta etapa, la atención se desplazó de las bibliotecas subyacentes hacia el cliente real.
La propuesta nunca iba a ser:
"Una aplicación de automatización de navegadores creada con Playwright y JavaScript."
Nadie busca específicamente Playwright; buscan un resultado.
Por lo tanto, la oferta pasó a ser el propio resultado. Algunos ejemplos:
Monitoreo de sitios web
Las empresas pueden hacer seguimiento a sus propios sitios web y recibir notificaciones cuando las páginas clave cambian o dejan de funcionar.
Pruebas de calidad automatizadas
Los equipos de ingeniería pueden realizar pruebas en el navegador de manera consistente y repetible en sus propias aplicaciones.
Automatización de flujos de trabajo internos
Las organizaciones pueden automatizar tareas repetitivas basadas en el navegador dentro de las herramientas a las que ya tienen acceso.
Automatización de informes
Un trabajo programado puede recopilar datos aprobados, guardarlos y compilarlos automáticamente en un informe.
Automatización para agencias
Una agencia puede diseñar pipelines de automatización personalizados para clientes y facturar por la configuración y el soporte continuo.
Los precios pueden adoptar varias formas:
- Un cargo único fijo por configuración
- Cargos mensuales recurrentes de mantenimiento
- Precio por flujo de trabajo individual
- Licencias basadas en equipos
- Trabajo de integración personalizada
- Acceso basado en suscripción y hospedado
La clave está en vincular la solución a un problema concreto y específico, en lugar del conjunto de tecnologías utilizado.
12. Los scripts pequeños pueden convertirse en productos reales
Al final, la arquitectura general del sistema se veía más o menos así:
User
│
▼
CLI / AI Input
│
▼
Task Validator
│
▼
Workflow Engine
│
┌────────┼────────┐
▼ ▼ ▼
Playwright Cheerio SQLite
│ │ │
└────────┼────────┘
▼
Result Data
│
▼
Report / API
Esa es la parte con la que vale la pena trabajar.
El problema subyacente nunca fue realmente sobre la automatización de navegadores en sí.
Se trataba de eliminar el trabajo manual repetitivo.
Las bibliotecas — Playwright, Cheerio, SQLite, Commander — eran simplemente los mecanismos para transformar esa repetición en software funcional.
Esa mentalidad determina ahora cómo se abordan los nuevos proyectos en JavaScript.
Cada vez que una secuencia de pasos manuales comienza a repetirse por vigésima vez, el instinto no es:
"Es hora de recurrir a un nuevo framework."
Sino:
“¿Se podría convertir este flujo de trabajo en una función?”
Si la respuesta es sí, esa es la semilla de un proyecto de automatización.
Y cuando dicha automatización ahorra suficiente tiempo y esfuerzo a alguien, puede convertirse en algo que valga la pena vender.
Lecturas relacionadas
- Diseño de API en Node.js con capas: de controladores complejos a arquitectura limpia — Aprenda cómo refactorizar una API de Node.js en capas de controlador, servicio y acceso a datos para solucionar lógica empresarial enredada, errores inconsistentes y problemas de escalabilidad.