Node.js
The SDK is a plain Node library — scripts, workers, and small servers. It never runs in the browser; see Core concepts for how files become jobs and URLs.
One client, reused
Export a shared instance from tk.ts. Install and your first transform → 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 slot opens — same pattern on Pipelines → Run a pipeline.
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);
}Fan out with a pipeline
Linear chains use runQueue; one input → many 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 [photo] = await tk.runPipeline(
[{ bytes: await readFile('photo.png'), filename: 'photo.png' }],
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 / output_targets → HTTP → Bring your own storage.
.upload(fn)— you store the source and return a GET URL for the worker..deliver(fn)— you 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 [photo] = await tk
.runQueue([{ bytes: await readFile('photo.png'), filename: 'photo.png' }], '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 — sign 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.TK_API_PUBLIC_URL!,
});export NODE_EXTRA_CA_CERTS=~/.portless/ca.pemNext steps
- Next.js — Server Actions and browser uploads.
- HTTP & curl — raw endpoints behind the SDK.