Si estás creando una aplicación Node.js que recibe documentos subidos por usuarios, probablemente hayas mirado una carpeta llena de PDF, archivos DOCX y presentaciones PPTX preguntándote: ¿cómo normalizo todo esto para convertirlo en algo que mi pipeline de LLM pueda usar de verdad?
Este artículo explica cómo usar la API REST de AnyMD desde Node.js — con fetch, axios, tipos de TypeScript, streaming y patrones de middleware de Express para convertir documentos a Markdown a escala de producción.
¿Por qué obtener Markdown desde Node.js?
Las aplicaciones Node.js gestionan las subidas de archivos de forma natural — Multer, Busboy, middleware de Express. Pero convertir esas subidas en texto limpio y estructurado para pipelines RAG o datos de entrenamiento de IA suele implicar dependencias nativas pesadas (PyMuPDF, LibreOffice, pandoc) que no tienen cabida en un runtime de JS.
AnyMD te ofrece una API HTTP sin estado. Una llamada, una respuesta, Markdown limpio. Sin dependencias nativas, sin bibliotecas de formatos del lado del servidor y sin subprocesos que llamen a Python desde tu aplicación Node.js.
Inicio rápido con fetch (sin dependencias)
Node.js 18+ incluye fetch de forma global. La conversión más sencilla de una sola vez es:
const fs = require("fs");
const { readFile } = require("fs/promises");
async function convertToMarkdown(filePath, apiKey) {
const form = new FormData();
form.set("file", new Blob([await readFile(filePath)]), filePath);
const resp = await fetch("https://anymd.net/api/convert", {
method: "POST",
headers: { Authorization: `Bearer ${apiKey}` },
body: form,
});
if (!resp.ok) {
throw new Error(`AnyMD error ${resp.status}: ${await resp.text()}`);
}
return await resp.text();
}
// Usage
const md = await convertToMarkdown("report.docx", "your-api-key");
console.log(md);
Eso es todo. AnyMD detecta automáticamente el formato a partir de la extensión del archivo y devuelve Markdown con sabor a GitHub.
TypeScript con respuestas tipadas
Para proyectos TypeScript, añade tipos de retorno adecuados y gestión de errores:
interface ConversionResult {
markdown: string;
format: string;
pages: number;
duration: number;
}
interface ConversionError {
status: number;
message: string;
}
async function convertToMarkdownTyped(
filePath: string,
apiKey: string
): Promise<ConversionResult> {
const form = new FormData();
const buffer = await fs.promises.readFile(filePath);
form.set("file", new Blob([buffer]), filePath);
const start = performance.now();
const resp = await fetch("https://anymd.net/api/convert", {
method: "POST",
headers: { Authorization: `Bearer ${apiKey}` },
body: form,
});
const duration = performance.now() - start;
if (!resp.ok) {
const err: ConversionError = {
status: resp.status,
message: await resp.text(),
};
throw err;
}
return {
markdown: await resp.text(),
format: filePath.split(".").pop() || "unknown",
pages: parseInt(resp.headers.get("X-Pages") || "0", 10),
duration: Math.round(duration),
};
}
Middleware de Express para endpoints de subida
Así puedes integrar AnyMD en un servidor Express con Multer para las subidas de archivos:
import express from "express";
import multer from "multer";
import fs from "fs/promises";
const app = express();
const upload = multer({ dest: "/tmp/uploads/" });
const ANYMD_API = "https://anymd.net/api/convert";
app.post("/upload", upload.single("file"), async (req, res) => {
try {
const apiKey = req.headers["authorization"]?.replace("Bearer ", "");
if (!apiKey) return res.status(401).json({ error: "Missing API key" });
const form = new FormData();
const buffer = await fs.readFile(req.file.path);
form.set("file", new Blob([buffer]), req.file.originalname);
const anymdResp = await fetch(ANYMD_API, {
method: "POST",
headers: { Authorization: `Bearer ${apiKey}` },
body: form,
});
if (!anymdResp.ok) {
return res.status(anymdResp.status).json({
error: await anymdResp.text(),
});
}
const markdown = await anymdResp.text();
res.json({ markdown, pages: anymdResp.headers.get("X-Pages") });
} catch (err) {
res.status(500).json({ error: err.message });
} finally {
if (req.file) fs.unlink(req.file.path).catch(() => {});
}
});
app.listen(3000);
Procesamiento por lotes con axios y reintentos
Cuando tienes cientos de documentos que procesar, las llamadas secuenciales son demasiado lentas. Aquí tienes un procesador concurrente por lotes con axios:
import axios from "axios";
import FormData from "form-data";
import fs from "fs";
import path from "path";
import pLimit from "p-limit";
const API = "https://anymd.net/api/convert";
async function convertOne(filePath, apiKey) {
const form = new FormData();
form.append("file", fs.createReadStream(filePath), path.basename(filePath));
for (let attempt = 0; attempt < 3; attempt++) {
try {
const resp = await axios.post(API, form, {
headers: {
...form.getHeaders(),
Authorization: `Bearer ${apiKey}`,
},
maxContentLength: 50 * 1024 * 1024,
timeout: 30_000,
});
const outPath = filePath.replace(path.extname(filePath), ".md");
fs.writeFileSync(outPath, resp.data);
return { file: path.basename(filePath), ok: true };
} catch (err) {
if (attempt === 2) return { file: path.basename(filePath), ok: false, error: err.message };
const delay = Math.pow(1.5, attempt) * 1000;
await new Promise(r => setTimeout(r, delay));
}
}
}
async function batchConvert(dir, apiKey, concurrency = 10) {
const files = fs.readdirSync(dir).filter(f =>
/\.(docx|pdf|pptx|xlsx|epub|odt|rtf)$/i.test(f)
);
const limit = pLimit(concurrency);
const results = await Promise.all(
files.map(f => limit(() => convertOne(path.join(dir, f), apiKey)))
);
const ok = results.filter(r => r.ok).length;
console.log(`${ok}/${results.length} converted`);
return results;
}
await batchConvert("./documents", "your-api-key", 10);
Streaming de documentos grandes
Para documentos de más de 10 MB, usa el endpoint de streaming para evitar almacenar en memoria toda la salida Markdown.
Comparación con bibliotecas locales de Node.js
| Formato | Biblioteca de Node.js | AnyMD |
|---|---|---|
| DOCX | mammoth — sin soporte para PPTX/PDF/XLSX | Una API, más de 15 formatos |
pdf-parse — pierde tablas y encabezados | GFM, conserva la estructura | |
| PPTX | pptx2md — solo CLI, sin API | API REST, pipeline asíncrono |
| XLSX | xlsx — exportación CSV, sin Markdown | Tablas → tablas GFM |
| EPUB | epub2md — sin mantenimiento desde 2022 | API activa y versionada |
Rendimiento: Node.js hablando con Rust
AnyMD se ejecuta sobre un backend Rust/Axum.
| Formato | Tamaño del documento | AnyMD (ms) | Biblioteca local (ms) |
|---|---|---|---|
| DOCX | 50 KB (10 páginas) | 280 | 620 (mammoth) |
| 120 KB (15 páginas) | 410 | 2,100 (pdf-parse) | |
| PPTX | 200 KB (20 diapositivas) | 520 | 1,800 (pptx2md) |
| XLSX | 80 KB (5 hojas) | 340 | 950 (xlsx + formato manual) |
De Markdown a un pipeline RAG
Cuando tu aplicación Node.js ya tiene Markdown limpio, el siguiente paso es fragmentar y generar embeddings para la búsqueda vectorial. La salida de AnyMD es la entrada ideal para fragmentación semántica — se conservan los límites de encabezados, la estructura de las listas y los encabezados de tablas.
Precios
| Plan | Páginas/mes | Tamaño máximo de archivo | Precio |
|---|---|---|---|
| Gratis | 100 | 10 MB | $0 |
| Inicial | 500 | 25 MB | $19 |
| Pro | 5,000 | 50 MB | $99 |
| Empresa | Personalizado | Personalizado | Personalizado |
Un documento típico se convierte en menos de 0,5 segundos. 100 páginas gratis al mes, sin tarjeta de crédito.
Obtén tu clave de API gratuita y empieza hoy a convertir documentos a Markdown desde Node.js.