Node.js
The SDK is a plain Node library for scripts, workers, and small servers. It never runs in the browser. See Core concepts for how files become jobs and URLs.
Create a shared client
Export a shared instance from tk.ts. Install and run your first transform in the Quickstart.
tk.ts
import { TransformKit } from '@transform-kit/sdk';
export const tk = new TransformKit({ apiKey: process.env.API_KEY! });Batch a folder
Pass one entry per file. Use a lazy loader so bytes are read only when a concurrency slot opens. The same pattern is covered under Pipelines.
batch.ts
import { readdir, readFile } from 'node:fs/promises';
import path from 'node:path';
import { tk } from './tk';
const dir = 'photos';
const names = (await readdir(dir)).filter((n) => /\.(png|jpe?g|webp|avif|tiff?)$/i.test(n));
const results = await tk
.runQueue(
names.map((name) => async () => ({
bytes: await readFile(path.join(dir, name)),
filename: name,
})),
'image',
)
.maxSize(2048)
.convert({ format: 'webp', quality: 82 })
.options({
concurrency: 6,
onProgress: (e) => console.log(e.stage, e.filename ?? e.index),
});
for (const r of results) {
if (r.ok) console.log(r.filename, '→', r.outputs[0]!.media.url);
else console.error(r.filename, 'failed:', r.error);
}Run a multi-output pipeline
Linear chains use runQueue. One input with several outputs needs a graph. Define responsivePipeline on Pipelines, then:
fan-out.ts
import { readFile } from 'node:fs/promises';
import { tk } from './tk';
import { responsivePipeline } from './pipelines';
const filename = 'photo.png';
const [photo] = await tk.runPipeline(
[{ bytes: await readFile(filename), filename }],
responsivePipeline,
);
if (!photo.ok) throw new Error(photo.error);
for (const out of photo.outputs) {
console.log(out.output, out.media.url);
}Bring your own storage
Override upload and delivery so bytes never touch TransformKit storage. The HTTP shape of input_url and output_targets is in the HTTP & cURL guide.
.upload(fn): store the source and return a GET URL for the worker..deliver(fn): return a presigned PUT per output; the worker writes there.
byos.ts
import { GetObjectCommand, PutObjectCommand, S3Client } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { readFile } from 'node:fs/promises';
import { tk } from './tk';
const r2 = new S3Client({
region: 'auto',
endpoint: `https://${process.env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com`,
credentials: {
accessKeyId: process.env.R2_ACCESS_KEY_ID!,
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY!,
},
});
const BUCKET = 'my-media';
const filename = 'photo.png';
const [photo] = await tk
.runQueue([{ bytes: await readFile(filename), filename }], 'image')
.upload(async (file) => {
const key = `inputs/${file.filename}`;
await r2.send(new PutObjectCommand({ Bucket: BUCKET, Key: key, Body: file.bytes }));
return getSignedUrl(r2, new GetObjectCommand({ Bucket: BUCKET, Key: key }), { expiresIn: 3600 });
})
.maxSize(2048)
.convert({ format: 'webp', quality: 82 })
.deliver(async (t) => {
const key = `outputs/${t.output ?? t.outputId}-${t.index}.webp`;
const putUrl = await getSignedUrl(
r2,
new PutObjectCommand({ Bucket: BUCKET, Key: key, ContentType: 'image/webp' }),
{ expiresIn: 3600 },
);
const publicUrl = await getSignedUrl(
r2,
new GetObjectCommand({ Bucket: BUCKET, Key: key }),
{ expiresIn: 86400 },
);
return { putUrl, contentType: 'image/webp', publicUrl };
});
console.log(photo.outputs[0]!.media.url);Presigned URLs are short-lived. Dashboard retries reuse the same URLs, so sign them long enough for a retry window or treat BYOS as fire-once.
Local development
Point baseUrl at your local API and trust the portless CA so Node accepts .localhost TLS.
tk.ts
export const tk = new TransformKit({
apiKey: process.env.API_KEY!,
baseUrl: process.env.API_URL!,
});export NODE_EXTRA_CA_CERTS=~/.portless/ca.pem