Next.js
Run TransformKit from the App Router — Server Actions and Route Handlers. The API key stays on the server; the browser only sees finished URLs.
One server-only client
Create the client once in a module marked server-only. Key setup matches the Quickstart.
import 'server-only';
import { TransformKit } from '@transform-kit/sdk';
export const tk = new TransformKit({ apiKey: process.env.API_KEY! });API_KEY=tk_your_key_hereReuse a pipeline from the guide
Fan-out graphs live on Pipelines — copy responsivePipeline (or your own) into lib/pipelines.ts and import it from Server Actions and Route Handlers.
Server Action: transform an upload
A form posts a file to a Server Action; runPipeline uploads, submits, and waits.
'use server';
import { tk } from '@/lib/transform-kit';
import { responsivePipeline } from '@/lib/pipelines';
export async function transform(formData: FormData) {
const file = formData.get('file') as File;
const [result] = await tk.runPipeline(
[{ bytes: file, contentType: file.type, filename: file.name }],
responsivePipeline,
);
if (!result.ok) throw new Error(result.error);
return result.outputs.map((o) => ({ label: o.output, url: o.media.url }));
}Call it from a Client Component and render the results:
'use client';
import { useState, useTransition } from 'react';
import { transform } from './actions';
type Output = { label?: string; url: string };
export function UploadForm() {
const [outputs, setOutputs] = useState<Output[]>([]);
const [pending, startTransition] = useTransition();
function onSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
const data = new FormData(e.currentTarget);
startTransition(async () => setOutputs(await transform(data)));
}
return (
<form onSubmit={onSubmit}>
<input type="file" name="file" accept="image/*" required />
<button disabled={pending}>{pending ? 'Transforming…' : 'Transform'}</button>
{outputs.map((o) => (
<figure key={o.url}>
<img src={o.url} alt={o.label ?? 'output'} />
<figcaption>{o.label}</figcaption>
</figure>
))}
</form>
);
}Large files: presign on the server, PUT in the browser
Mint an upload ticket in a Server Action, let the browser PUT bytes straight to storage, then submit with input_key. That flow matches Core concepts and HTTP & curl; a working three-path demo (FormData vs managed presign vs BYOS) lives in the nextjs-upload-example repo.
Stream batch progress
For many files, a Route Handler can stream runPipeline's onProgress as newline-delimited JSON. Lazy loaders and concurrency → Node.js and Pipelines.
import { tk } from '@/lib/transform-kit';
import { responsivePipeline } from '@/lib/pipelines';
export const maxDuration = 300;
export async function POST(req: Request) {
const form = await req.formData();
const files = form.getAll('files') as File[];
const encoder = new TextEncoder();
const stream = new ReadableStream({
async start(controller) {
const send = (o: unknown) => controller.enqueue(encoder.encode(JSON.stringify(o) + '\n'));
const results = await tk.runPipeline(
files.map((f) => ({ bytes: f, contentType: f.type, filename: f.name })),
responsivePipeline,
{ concurrency: 4, onProgress: (e) => send({ type: 'progress', ...e }) },
);
send({ type: 'done', results });
controller.close();
},
});
return new Response(stream, { headers: { 'Content-Type': 'application/x-ndjson' } });
}Displaying results
Result URLs are signed and external — a plain <img> works. For next/image, allowlist the storage host under images.remotePatterns. Retention and copying bytes to your CDN → Core concepts.
Next steps
- Node.js — batch scripts and bring-your-own storage.
- Pipelines — build the graph you import here.
- Auth & billing — fan-out multiplies credits per output.