Jeśli tworzysz aplikację Node.js, która przyjmuje dokumenty przesyłane przez użytkowników, prawdopodobnie patrzyłeś już na folder pełen PDF-ów, plików DOCX i prezentacji PPTX, zastanawiając się: jak znormalizować to wszystko do postaci, której mój potok LLM rzeczywiście może użyć?
Ten artykuł pokazuje użycie REST API AnyMD z Node.js — z fetch, axios, typami TypeScript, streamingiem i wzorcami middleware Express do produkcyjnej konwersji dokumentów do Markdown.
Dlaczego Markdown z Node.js?
Aplikacje Node.js naturalnie obsługują przesyłanie plików — Multer, Busboy, middleware Express. Jednak zamiana tych plików w czysty, ustrukturyzowany tekst dla potoków RAG lub danych treningowych AI zwykle wymaga ciężkich natywnych bindingów (PyMuPDF, LibreOffice, pandoc), które nie pasują do środowiska JS.
AnyMD daje bezstanowe HTTP API. Jedno wywołanie, jedna odpowiedź, czysty Markdown. Bez natywnych zależności, bez serwerowych bibliotek formatów, bez podprocesów Pythona w aplikacji Node.js.
Szybki start z fetch (bez zależności)
Node.js 18+ udostępnia fetch globalnie. Najprostsza jednorazowa konwersja:
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);
To wszystko. AnyMD automatycznie rozpoznaje format po rozszerzeniu pliku i zwraca GitHub-Flavored Markdown.
TypeScript z typowanymi odpowiedziami
W projektach TypeScript dodaj odpowiednie typy zwracane i obsługę błędów:
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 Express dla endpointów uploadu
Tak podłączysz AnyMD do serwera Express z Multerem do obsługi przesyłania plików:
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);
Przetwarzanie wsadowe z axios i ponowieniami
Gdy masz setki dokumentów do przetworzenia, wywołania sekwencyjne są zbyt wolne. Oto współbieżny procesor wsadowy z 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 dużych dokumentów
Dla dokumentów większych niż 10 MB użyj endpointu streamingowego, aby nie buforować całego wyniku Markdown w pamięci.
Porównanie z lokalnymi bibliotekami Node.js
| Format | Biblioteka Node.js | AnyMD |
|---|---|---|
| DOCX | mammoth — brak obsługi PPTX/PDF/XLSX | Jedno API, ponad 15 formatów |
pdf-parse — gubi tabele i nagłówki | GFM, zachowuje strukturę | |
| PPTX | pptx2md — tylko CLI, bez API | REST API, potok asynchroniczny |
| XLSX | xlsx — eksport CSV, bez Markdown | Tabele → tabele GFM |
| EPUB | epub2md — nieutrzymywana od 2022 roku | Aktywne, wersjonowane API |
Wydajność: Node.js rozmawia z Rust
AnyMD działa na backendzie Rust/Axum.
| Format | Rozmiar dokumentu | AnyMD (ms) | Biblioteka lokalna (ms) |
|---|---|---|---|
| DOCX | 50 KB (10 stron) | 280 | 620 (mammoth) |
| 120 KB (15 stron) | 410 | 2,100 (pdf-parse) | |
| PPTX | 200 KB (20 slajdów) | 520 | 1,800 (pptx2md) |
| XLSX | 80 KB (5 arkuszy) | 340 | 950 (xlsx + ręczne formatowanie) |
Od Markdown do potoku RAG
Gdy aplikacja Node.js ma już czysty Markdown, następnym krokiem jest chunking i embedding dla wyszukiwania wektorowego. Wynik AnyMD jest idealnym wejściem dla chunkingu semantycznego — granice nagłówków, struktura list i nagłówki tabel są zachowywane.
Cennik
| Plan | Strony/miesiąc | Maksymalny rozmiar pliku | Cena |
|---|---|---|---|
| Bezpłatny | 100 | 10 MB | $0 |
| Starter | 500 | 25 MB | $19 |
| Pro | 5,000 | 50 MB | $99 |
| Enterprise | Niestandardowy | Niestandardowy | Niestandardowy |
Typowy dokument konwertuje się w mniej niż 0,5 sekundy. 100 bezpłatnych stron miesięcznie — bez karty kredytowej.
Pobierz bezpłatny klucz API i zacznij już dziś konwertować dokumenty do Markdown z Node.js.