Node.js и TypeScript
Типы в комплекте, понятные ошибки, повторы при перегрузке. Без зависимостей: работает в Node.js, Bun и Deno; ESM и CommonJS.
| Пакет | Среды | Зависимости |
|---|---|---|
pdfapi | Node.js 20.3+, Bun, Deno | нет |
Установка#
npm install pdfapiКлюч берётся из переменной окружения PDFAPI_KEY — выпустите его в кабинете:
export PDFAPI_KEY=pdf_live_...Первый PDF#
import { PdfApi } from 'pdfapi';
const client = new PdfApi(); // ключ из PDFAPI_KEY
const pdf = await client.html('<h1>Счёт №42</h1><p>К оплате: 12 000 ₽</p>', { marginTop: '20mm' });
await pdf.save('invoice.pdf');
console.log(pdf.pages); // 1 — сколько страниц списано
console.log(pdf.quota?.remaining); // 9857 — сколько осталось в периодеВ CommonJS — то же самое через const { PdfApi } = require('pdfapi').
Методы#
Все методы асинхронные и возвращают Promise.
| Метод | Что делает |
|---|---|
client.html(html, options?) | HTML → PDF |
client.url(url, options?) | публичная страница по адресу → PDF. Тариф Premium и выше; адрес — полный, с https:// |
client.markdown(text, options?) | Markdown → PDF. Тариф Premium и выше |
client.office(file, options?) | docx, xlsx, pptx → PDF. Тариф Premium и выше |
client.convert(body, options?) | POST /v1/convert с телом как есть — для всего остального |
client.me(options?) | тариф, расход и остаток страниц — GET /v1/me |
Параметры страницы#
Все параметры необязательные. Что они значат и как сочетаются, смотрите в разделе Параметры страницы.
| Параметр | Пример | По умолчанию |
|---|---|---|
landscape | true | false |
paperWidth, paperHeight | '8.27in', '297mm' | A4 |
marginTop, marginBottom, marginLeft, marginRight | '20mm' | '0.39in' |
waitDelay | '2s', '500ms', до 10 секунд | '0s' |
const pdf = await client.url('https://example.com/report', { landscape: true, waitDelay: '1s' });Размер — число и единица pt, px, in, mm, cm или pc без пробела. 20, '20' и '20 mm' вызовут RangeError сразу, без запроса к API.
В тех же options передаются signal и timeout — см. Отмена и таймауты.
Документы Office#
const fromPath = await client.office('report.docx'); // путь (Node, Bun, Deno)
const fromBytes = await client.office(buffer, { filename: 'report.docx' }); // байты — нужно имя с расширением
const fromForm = await client.office(formData.get('file') as File); // File из формы — имя берётся из негоfile — путь, Uint8Array или Buffer, ArrayBuffer, Blob или File. Файл — до 3,7 МБ: в base64 он вырастает на треть, а тело запроса ограничено 5 МБ. Библиотека проверяет, что файл — настоящий ZIP-архив, поэтому старый .doc или PDF, переименованный в .docx, отклоняется до отправки.
Результат#
PdfResult:
content— байты PDF,Uint8Array;pages— сколько страниц списано;quota—limit,usedиremainingпосле этой конвертации;save(path)— записать в файл и вернуть путь;blob()— PDF какBlobс типомapplication/pdf;size— размер в байтах.
save пишет атомарно: сначала во временный файл рядом, потом переименовывает поверх. Оборвётся запись или кончится место — останется прежний файл, а не обрезанный PDF. console.log(pdf) байты не печатает — только страницы, размер и квоту.
Аккаунт и квота#
const me = await client.me();
console.log(me.plan, me.pagesRemaining, 'из', me.pagesLimit);
console.log('обнулится', me.periodEnd?.toISOString());Account: userId, plan, periodStart, periodEnd (Date или null), pagesUsed, pagesLimit, pagesRemaining, requestsPerSecond, burst. Запрос страниц не тратит. Как встроить проверку в cron — в руководстве Контроль квоты.
Ошибки#
Все ошибки API — наследники PdfApiError с полями code, status и message по-русски.
import { PdfApiError, QuotaExceededError, RateLimitError, RenderError } from 'pdfapi';
try {
const pdf = await client.html(html);
} catch (e) {
if (e instanceof QuotaExceededError) console.log('Страницы кончились:', e.message);
else if (e instanceof RateLimitError) console.log(`Слишком часто, повторите через ${e.retryAfter} с`);
else if (e instanceof RenderError) console.log('Документ не отрендерился — проверьте разметку');
else if (e instanceof PdfApiError) console.log(e.code, e.status, e.message);
else throw e;
}| Ошибка | Когда |
|---|---|
AuthenticationError | 401: ключа нет, он неверный или отозван |
PermissionDeniedError | 403: почта не подтверждена или формат недоступен на тарифе |
QuotaExceededError | 402: страницы периода кончились или документ больше остатка |
InvalidRequestError | 400: неверный параметр или адрес во внутреннюю сеть (code === 'url_not_allowed') |
PayloadTooLargeError | 413: тело больше 5 МБ; наследник InvalidRequestError |
RateLimitError | 429: слишком часто; retryAfter — через сколько секунд можно снова |
RenderError | 502: документ не отрендерился |
RenderTimeoutError | 504: рендер дольше 30 секунд; наследник RenderError |
UnavailableError | 503: рендер временно недоступен |
APIConnectionError | нет связи с API |
APITimeoutError | API не ответил за timeout; наследник APIConnectionError |
Ошибки в аргументах — пустой HTML, адрес без https://, файл .pdf вместо .docx — обычные TypeError и RangeError. Они приходят отклонённым Promise, как и ошибки API, но до запроса и без траты лимита частоты. Все коды API — в разделе Ошибки и повторы.
Повторы#
Библиотека сама повторяет запрос, когда это безопасно, — по умолчанию до двух раз (maxRetries):
- 429 — через
Retry-Afterсекунд, но не дольше минуты; - 503 или нет соединения (
ECONNREFUSED, DNS) — первый повтор примерно через 0,5 секунды, второй — примерно через 1 секунду.
После таймаута результат может быть уже сохранён. Повтор с тем же Idempotency-Key и телом запроса защищает от повторного списания на обновлённом сервере. Без этого ключа повтор считается новой конвертацией. Подробнее.
Отмена и таймауты#
const controller = new AbortController();
const pdf = await client.html(html, { signal: controller.signal, timeout: 10_000 });timeout— миллисекунды на одну попытку; по умолчанию — таймаут клиента, 40 000.- Отменили через
signal—Promiseотклоняется вашей причиной изcontroller.abort(reason), а не ошибкой API.
Настройки клиента#
const client = new PdfApi({
apiKey: 'pdf_live_...', // иначе PDFAPI_KEY
baseUrl: 'https://pdfapi.ru/api', // иначе PDFAPI_BASE_URL
timeout: 40_000, // мс на один запрос
maxRetries: 2,
fetch: myFetch, // свой fetch — например, через прокси
});- Один клиент на приложение. Создайте его один раз и переиспользуйте — закрывать ничего не нужно.
- Неверные настройки —
timeoutне больше нуля,maxRetriesменьше нуля или дробное —RangeErrorсразу. - Ключ не утекает — он в приватном поле: его нет ни в
console.log(client), ни вJSON.stringify(client), ни в текстах ошибок.
Ключ в коде страницы увидит любой пользователь, поэтому в браузере new PdfApi() выбрасывает ошибку. Вызывайте API со своего сервера, а браузеру отдавайте готовый PDF.
PDF в веб-приложении#
PDF уже в памяти — отдавайте байты, на диск сохранять незачем:
// app/invoice/route.ts — так же в Hono и любом обработчике на Request/Response
import { PdfApi } from 'pdfapi';
const client = new PdfApi();
export async function GET() {
const pdf = await client.html('<h1>Счёт №42</h1>');
return new Response(pdf.blob(), {
headers: { 'Content-Disposition': 'inline; filename="invoice.pdf"' },
});
}Данные пользователя в HTML — только с экранированием, иначе это HTML-инъекция в документ — см. Безопасность и данные.
Deno#
import { PdfApi } from 'npm:pdfapi';deno run --allow-net=pdfapi.ru --allow-env=PDFAPI_KEY,PDFAPI_BASE_URL app.ts--allow-env не нужен, если ключ и адрес переданы в new PdfApi({...}) явно. Для save() и office('путь') добавьте --allow-write и --allow-read.
Примеры#
В папке examples/ библиотеки; Node.js 22.18+ запускает их без компиляции: npm run build && PDFAPI_KEY=pdf_live_... node examples/<файл>.
| Файл | Что показывает |
|---|---|
invoice.ts | счёт из HTML-шаблона с экранированием данных |
from-url.ts | страница по адресу, альбомная, с ожиданием скриптов |
office.ts | docx, xlsx или pptx в PDF рядом с исходным файлом |
batch.ts | пачка документов параллельно, в пределах лимита частоты |
quota.ts | остаток страниц для cron: код выхода 2 при нехватке |
server.ts | отдача PDF из node:http, ошибки API — в HTTP-ответы |