TypeScript SDK cho PDF API: năm phút để có lần gọi đầu tiên, và những gì SDK để lại cho bạn
Dùng @pdf123/sdk từ TypeScript để gộp tệp, thêm hình mờ, đọc thông tin tệp và nối nhiều công cụ thành một yêu cầu. SDK bắt lỗi gõ sai tên công cụ hoặc tham số trước khi tải lên bất cứ thứ gì; khi máy chủ từ chối, lỗi có kèm lý do và gợi ý, còn thử lại, hủy và lỗi từng phần trong một lô là việc của bạn.

Khi gọi PDF API từ Node, bộ công cụ phát triển (SDK) @pdf123/sdk bắt lỗi gõ sai tên công cụ hoặc tham số trước khi bất kỳ tệp nào được tải lên. Nó không tự thử lại, không hủy thay bạn và không trả kết quả theo luồng. Mặc định new Pdf123Client() trỏ ẩn danh tới https://pdf123.xyz; tệp của bạn được tải lên đó và xử lý tại đó, vì không có engine chạy cục bộ.
Hai tệp PDF trong một thư mục là đủ để gộp
Bạn cần Node 20.3 trở lên, hoặc Bun. Gói này là ES module: hãy đặt mã vào tệp .mjs, hoặc chạy npm pkg set type=module trước. Đặt a.pdf và b.pdf vào thư mục hiện tại.
npm install @pdf123/sdk
// merge.mjs
import { Pdf123Client } from "@pdf123/sdk";
import { readFileInput, saveResult } from "@pdf123/sdk/node";
const client = new Pdf123Client();
const merged = await client.run("merge", {
input: [await readFileInput("a.pdf"), await readFileInput("b.pdf")],
});
console.log(await saveResult(merged, { output: "merged.pdf" }));
Sau node merge.mjs, terminal in ra đường dẫn đầu ra bạn đã truyền, merged.pdf, và tệp nằm trong thư mục hiện tại. readFileInput và saveResult nằm trong @pdf123/sdk/node. Entry gốc chỉ phụ thuộc vào fetch và nhận đầu vào dạng { name, data } thuần, nên môi trường không có hệ thống tệp vẫn dùng được cùng client này. Một kết quả có dạng { data, contentType, filename, json }; data là một Uint8Array nguyên vẹn, và Buffer.from(result.data) cho bạn một Buffer.
Gộp PDF chỉ là một trong các công cụ.
Tệp trả về trong data, báo cáo trong json
Mỗi công cụ được gọi bằng client.run(toolName, { input, params }). Đoạn mã dưới đây tiếp nối merge.mjs ở phần trước, với client và saveResult đã có sẵn. Đọc kết quả ở đâu tùy vào trường returns từ getTool, giá trị là "file" hoặc "json": dùng data cho loại đầu, json cho loại sau.
import { getTool } from "@pdf123/sdk";
import { readFileInput, saveResult } from "@pdf123/sdk/node";
// client comes from merge.mjs in the previous section
const marked = await client.run("watermark", {
input: await readFileInput("a.pdf"),
params: { watermarkText: "DRAFT", fontSize: 40 },
});
console.log(await saveResult(marked, { output: "marked.pdf" }));
const info = await client.run("get-info", { input: await readFileInput("a.pdf") });
console.log(info.json.FileSize, info.json.Encrypted);
console.log(getTool("get-info")?.returns); // "json"
Khối đầu ghi tệp đã đóng hình mờ vào marked.pdf; ở khối thứ hai, info.json là báo cáo. Bạn không cần nhớ tên trường, giá trị mặc định hay các giá trị cho phép: getTool("watermark")?.fields chính là danh mục đó, và pdfx describe watermark trên dòng lệnh đọc cùng một danh mục. TOOLS chứa đủ 95 công cụ. Tài liệu tham khảo đầy đủ nằm trong hướng dẫn SDK ở trang dành cho nhà phát triển.
Để gộp, rồi thêm hình mờ, rồi nén, bạn có thể dùng pipeline hoặc ba lần gọi run liên tiếp; chọn cách nào tùy bạn có cần các tệp trung gian hay không. Đoạn này cũng tiếp nối client ở trên. pipeline gộp ba bước vào một yêu cầu, kết quả trung gian ở lại máy chủ, và bên gọi chỉ nhận bước cuối, mà mã dưới đây ghi vào out.pdf. Nếu bạn muốn có tệp của từng bước, hãy gọi riêng từng công cụ.
const result = await client.pipeline(
[{ tool: "merge" }, { tool: "watermark", params: { watermarkText: "DRAFT" } }, { tool: "compress" }],
[await readFileInput("a.pdf"), await readFileInput("b.pdf")],
);
console.log(await saveResult(result, { output: "out.pdf" }));
Một pipeline có tối đa 8 bước. Bước thứ 9 nhận HTTP 400: at most 8 pipeline steps allowed.
Ẩn danh, dùng khóa, hoặc trỏ tới máy chủ của bạn
Nếu bạn đã đặt PDFX_API_BASE mà vẫn gọi tới https://pdf123.xyz, đó là vì new Pdf123Client() không đọc biến môi trường; nó chỉ xem các tùy chọn của constructor.
Để gọi ẩn danh, dùng constructor đó nguyên trạng. Với khóa, viết new Pdf123Client({ apiKey: process.env.PDFX_API_KEY }); tiêu đề yêu cầu là X-API-KEY.
Để trỏ tới máy chủ của riêng bạn, dùng clientFromEnv() từ @pdf123/sdk/node. Hàm này đọc PDFX_API_BASE và PDFX_API_KEY. Bạn cũng có thể viết thẳng new Pdf123Client({ baseUrl: "http://localhost:8080" }). Nếu địa chỉ không truy cập được thì lời gọi thất bại; không có chế độ ngoại tuyến. Bạn được gì và mất gì khi tệp ở lại trong mạng của mình được trình bày trong Self-host thực sự mua được gì (và tốn gì).
Tham số sai bị bắt trước khi tải lên
Nếu không có cơ chế này, tệp sẽ được tải lên trước và tên trường gõ sai chỉ lộ ra dưới dạng lỗi 400 từ máy chủ. Kiểu tham số của từng công cụ được sinh từ danh mục, nên ba lỗi vặt thường gặp sẽ thất bại ngay ở giai đoạn tsc:
| Bạn viết | Lỗi trình biên dịch (trích) |
|---|---|
client.run("rotat", { input }) |
Argument of type '"rotat"' is not assignable to parameter of type 'ToolId' |
params: { watermarkTxt: "DRAFT" } |
Object literal may only specify known properties, but 'watermarkTxt' does not exist, kết thúc bằng Did you mean to write 'watermarkText'? |
params: { angle: 45 } (rotate chỉ nhận 90, 180, 270) |
Type '45' is not assignable to type …, tiếp theo là các giá trị cho phép |
Các lời gọi đúng như { angle: 90 } hay { watermarkText: "DRAFT", fontSize: 30 } biên dịch bình thường. Các trường số nhận cả số lẫn chuỗi, nên fontSize: 30 và fontSize: "30" tương đương nhau. Dự án không dùng TypeScript nhận cùng những lỗi này lúc chạy, và yêu cầu không bao giờ được gửi đi: trường hợp thứ ba cho Field "angle" of "rotate" must be one of: 90, 180, 270, còn gõ sai tên công cụ cho unknown_tool, kèm các tên tương tự trong thông báo.
Khi máy chủ từ chối, hãy rẽ nhánh theo reason
Khi máy chủ từ chối một yêu cầu, SDK ném Pdf123Error với status, code, reason và problem.hint. Một code có thể ứng với nhiều giá trị reason, nên hãy kiểm tra reason trước rồi mới quay về code. Ba lỗi thường gặp, chạy cục bộ với cùng đầu vào, trông như sau:
| Đầu vào | status | code | reason | hint (bản gốc bằng tiếng Anh) |
|---|---|---|---|---|
| PDF được mã hóa, không đưa mật khẩu | 400 | bad_request |
password_required |
Provide the document password, or unlock the PDF first |
| Sai mật khẩu | 400 | bad_request |
wrong_password |
Check the password and try again |
| Không phải PDF, hoặc tệp bị hỏng | 400 | invalid_document |
invalid_pdf |
Upload a valid, undamaged PDF; you can try repairing it first |
import { Pdf123Client, Pdf123Error } from "@pdf123/sdk";
import { readFileInput } from "@pdf123/sdk/node";
const client = new Pdf123Client();
const input = await readFileInput("locked.pdf");
try {
await client.run("compress", { input });
} catch (error) {
if (!(error instanceof Pdf123Error)) throw error;
console.error(error.status, error.code, error.reason, error.problem?.hint);
if (error.reason === "password_required") {
await client.run("compress", { input, password: "secret" });
}
}
Khi biết mật khẩu, hãy truyền password cho một tệp; việc mở khóa và công cụ đích diễn ra trong cùng một yêu cầu. Với tệp hỏng, hãy thử Sửa PDF trước.
Cần phân biệt "không có kết quả". Khi PDF sang CSV không tìm thấy bảng nào trong PDF, máy chủ trả 204 và SDK ném code: "no_content"; lý do được giải thích trong Xuất bảng trống (204): PDF của bạn có lẽ không có cột. Các công cụ lọc theo điều kiện coi điều kiện sai là kết quả bình thường: chúng không ném lỗi, mà trả matched: false với data rỗng.
Thử lại, bộ nhớ và hủy là việc của bên gọi
Lỗi mạng và phản hồi 5xx được ném nguyên trạng; SDK không bao giờ tự thử lại. Với yêu cầu làm thay đổi trạng thái, hãy truyền idempotencyKey của riêng bạn: gửi lại với cùng khóa sẽ trả kết quả đầu tiên thay vì xử lý lại, còn cơ chế và giới hạn nằm trong Idempotency-Key: thử lại an toàn cho tác vụ PDF. Kết quả được đọc nguyên vẹn vào bộ nhớ, không có streaming. Khi cả đầu vào lẫn đầu ra đều lớn, hãy tính lượng bộ nhớ chúng chiếm cùng lúc.
Hủy và hết thời gian chờ là hai giá trị code khác nhau, và một lời gọi chỉ báo cáo cái xảy ra trước. Ví dụ dưới đây kiểm tra chúng trong hai lời gọi riêng, để cả hai nhánh đều thực sự chạy được:
import { Pdf123Client, Pdf123Error } from "@pdf123/sdk";
import { readFileInput } from "@pdf123/sdk/node";
const client = new Pdf123Client();
const input = await readFileInput("a.pdf");
const controller = new AbortController();
setTimeout(() => controller.abort(), 10_000);
try {
await client.run("compress", { input, signal: controller.signal });
} catch (error) {
if (error instanceof Pdf123Error && error.code === "cancelled") { /* bạn đã hủy */ }
}
try {
await client.run("compress", { input, timeoutMs: 60_000 });
} catch (error) {
if (error instanceof Pdf123Error && error.code === "timeout") { /* vượt quá timeoutMs; lần này không có signal */ }
}
Ngay khi tín hiệu hủy (AbortSignal) được kích hoạt, yêu cầu bị từ chối với code: "cancelled"; vượt quá thời gian chờ cho code: "timeout". Mỗi yêu cầu có thời gian chờ mặc định 5 phút, bạn có thể đổi khi tạo client hoặc ghi đè cho từng lời gọi bằng timeoutMs. Với tải lên theo từng phần, mỗi yêu cầu mang thời gian chờ đó riêng. Hủy chỉ đóng kết nối này; không có gì đảm bảo máy chủ sẽ dừng xử lý.
Một tệp lỗi, các tệp còn lại vẫn chạy, và callback đến theo thứ tự hoàn thành
Với một lô đầu vào cho công cụ xử lý một tệp, hãy dùng runBatch. Nếu một tệp lỗi, các tệp khác vẫn tiếp tục. onResult được gọi ngay khi từng tệp xong, nên nó chạy theo thứ tự hoàn thành, và index là vị trí của tệp trong mảng đầu vào; mảng trả về luôn theo thứ tự đầu vào. Hãy ghi byte ra đĩa ngay trong callback này: retainData: false làm rỗng data khi callback trả về, nên kết quả nào bạn không saveResult ở đây sẽ mất. Nếu lần chạy bị gián đoạn, các tệp đã ghi vẫn còn.
Giả sử thư mục có ba tệp PDF hợp lệ, a.pdf, b.pdf và locked.pdf được mã hóa (mật khẩu secret), cộng thêm một tệp hỏng broken.pdf chỉ chứa vài ký tự gõ tay:
import { Pdf123Client } from "@pdf123/sdk";
import { fileSource, saveResult } from "@pdf123/sdk/node";
const client = new Pdf123Client();
const results = await client.runBatch("compress", [
fileSource("a.pdf"), fileSource("broken.pdf"), fileSource("b.pdf"), fileSource("locked.pdf"),
], {
concurrency: 2,
passwordFor: (file) => (file.name === "locked.pdf" ? "secret" : undefined),
onResult: async (entry, index) => {
console.log(index, entry.input.name, entry.ok ? "ok" : entry.error.reason);
if (entry.ok) await saveResult(entry.result, { output: "out/" });
},
retainData: false,
});
Mặc định hai tệp được xử lý cùng lúc; đổi bằng concurrency. fileSource(path) chỉ đọc tệp từ đĩa khi đến lượt, nên một lô lớn gồm các tệp lớn không bao giờ nằm hết trong bộ nhớ cùng lúc. passwordFor cho phép một lô trộn lẫn tệp khóa và không khóa chạy trọn trong một lần. Chạy bốn tệp trên cục bộ, callback nhận được:
1 broken.pdf invalid_pdf
0 a.pdf ok
2 b.pdf ok
3 locked.pdf ok
Thứ tự hoàn thành có thể khác nhau ở mỗi lần chạy; đây chỉ là dáng vẻ của một lần chạy. retainData: false làm data trong mảng trả về rỗng; các tệp thành công đã được saveResult ở trên ghi vào out/. Khi bạn truyền idempotencyKey cho runBatch, khóa thực sự được gửi cho từng tệp là <key>:<index>.
Tải lên theo từng phần chỉ bắt đầu khi tổng vượt 95 MiB
Thân yêu cầu gửi trực tiếp bị giới hạn ở 100 MiB, và lớn hơn thì bị từ chối; chi tiết xem Khi một tệp PDF lớn bị từ chối: giới hạn 100 MiB cho thân yêu cầu và lỗi chỉ sai hướng. Khi tất cả tệp trong một yêu cầu cộng lại vượt 95 MiB, SDK chuyển sang tải lên theo từng phần, và lời gọi client.run của bạn không đổi. Mức tối đa mặc định của máy chủ cho một lần tải lên là 500 MiB; khi tự host, hãy điều chỉnh bằng PDFX_UPLOAD_MAX_BYTES.
Muốn xem cùng một thao tác viết bằng curl, MCP và dòng lệnh, hãy đọc Cùng một thao tác, bốn client: trình duyệt, curl, MCP, pdfx: bài đó đặt bốn lời gọi cạnh nhau, còn bài này chỉ mở rộng phần SDK. Trang của gói trên npm là @pdf123/sdk.