コンテンツにスキップ

TypeScript

composepdf はこの API の型つきクライアントです。依存パッケージを持たず、レイアウト エンジンも積んでいません。レンダリングはサーバー側のままなので、返る PDF は デザイナーがプレビューで見たものと同じです。

Terminal window
pnpm add composepdf

Node 18 以降・Bun・Deno・Cloudflare Workers など、fetch がある場所で動きます。

import { writeFile } from 'node:fs/promises';
import { createClient } from 'composepdf';
const composepdf = createClient({ apiKey: process.env.COMPOSEPDF_API_KEY! });
const { bytes } = await composepdf.render('cv_…', {
data: {
customer: { name: '株式会社Acme' },
items: [
{ name: 'デザイン作業', price: 1200 },
{ name: 'ホスティング', price: 90 },
],
},
});
await writeFile('invoice.pdf', bytes);

キーは持っている人がそのまま使える資格情報なので、サーバー側に置いてください。 自社の画面にプレビューを出したい場合は埋め込みライブプレビューを 使います。あちらは範囲を限定した埋め込みトークンで動きます。

テンプレートから型を生成する

Section titled “テンプレートから型を生成する”

公開したテンプレートは、どのデータを読むかを 自分で宣言しています。composepdf types はその宣言を1つのファイルにまとめます。

Terminal window
pnpm exec composepdf types --out src/composepdf-types.ts

生成された型をクライアントに渡すと、テンプレートの id と送るデータの形の両方が コンパイル時に検査されます。

import { createClient } from 'composepdf';
import type { Templates } from './composepdf-types';
const composepdf = createClient<Templates>({ apiKey: process.env.COMPOSEPDF_API_KEY! });
await composepdf.render('cv_…', { data: { customer: { name: '株式会社Acme' } } });
// ^ このキーで焼ける id だけ
// ^ このテンプレートが宣言した欄だけ

手で書き足すものはありません。中身はテンプレート自身のデータスキーマなので、 欄を足して公開し直したら生成し直す、それが移行作業の全部です。生成物は コミットしてください —— 日時を持たないので、変わっていないワークスペースを 生成し直しても差分は出ません。

コマンドは環境変数 COMPOSEPDF_API_KEY を読みます(--key で上書き)。出力先は --out を省くと composepdf-types.ts です。

エラーは、どの経路が違うかを名指しする

Section titled “エラーは、どの経路が違うかを名指しする”

テンプレートが受け取れないデータはレンダリングが始まる前に弾かれます。間違った 呼び出しに費用はかからず、どこが違うかがそのまま返ります。

import { ComposePdfError } from 'composepdf';
try {
await composepdf.render('cv_…', { data });
} catch (e) {
if (e instanceof ComposePdfError && e.code === 'data_contract_violation') {
for (const issue of e.issues) {
console.error(issue.path, issue.message); // items[3].price expected number
}
}
}

ComposePdfErrorstatusrequestId(問い合わせの際に伝えてください)、 429503 のときの retryAfter も持ちます。

const { bytes, records } = await composepdf.render('cv_…', {
dataList: [invoiceA, invoiceB, invoiceC],
});

1レコードにつき1文書を、順番どおりに1つのファイルへ綴じます。詳しくは まとめて1ファイルに

const job = await composepdf.renderAsync('cv_…', { dataList: manyRecords });
let render = await composepdf.getRender(job.id);
while (!render.done) {
await new Promise((r) => setTimeout(r, 1000));
render = await composepdf.getRender(job.id);
}
const bytes = await composepdf.downloadRender(job.id);

受け付けられたジョブは必ず PDF を保持します。同期レンダリングが保持するのは options: { store: true } を付けたときだけです。

await composepdf.render('cv_…', { data }, { idempotencyKey: `order-${orderId}` });

同じキーをもう一度出すと、保存済みの PDF がそのまま返ります。焼き直しも二重の 計上も起きません。

クライアントの面は公開 OpenAPI ドキュメントと 同一です —— listTemplates / render / renderAsync / getSchema / getSchemaSource / listRenders / getRender / downloadRender / getUsage / healthcurl で届くものは、ここから型つきで届きます。