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.

lib/transform-kit.ts
import 'server-only';
import { TransformKit } from '@transform-kit/sdk';

export const tk = new TransformKit({ apiKey: process.env.API_KEY! });
.env.local
API_KEY=tk_your_key_here

Reuse 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.

app/actions.ts
'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:

app/upload-form.tsx
'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>
  );
}
The file travels throughyour server, so you hit the deploy target's body limit (Vercel's default is 4.5 MB). For larger files, use presigned browser uploads — see below.

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.

app/api/batch/route.ts
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.