Next.js

Start renders, poll progress, and download output with Next.js App Router route handlers and a browser hook.

Next.js App Router route handlers run on the server, so they can import the SDK directly. This guide uses three route handlers plus a client hook.

Keep lib/sdk.ts out of any "use client" module. The SDK pulls in Node-only Remotion packages and must never be bundled for the browser.


Shared SDK instance

// lib/sdk.ts
import { RenderSdk } from "@remocn/render-sdk";
import { RenderServer } from "@remocn/render-sdk/server";

export const sdk = new RenderSdk({
  adapter: RenderServer({
    serveUrl: process.env.REMOTION_SERVE_URL!,
    workDir: process.env.RENDER_WORK_DIR ?? "./out",
    publicUrl: process.env.RENDER_PUBLIC_URL,
    concurrency: 2,
  }),
});

Start route

// app/api/render/route.ts
import { NextResponse } from "next/server";
import { sdk } from "@/lib/sdk";

export async function POST(req: Request) {
  const { compositionId, inputProps } = await req.json();

  const handle = await sdk.start({ compositionId, inputProps });

  return NextResponse.json({ handle });
}

Poll route

// app/api/render/[handle]/route.ts
import { NextResponse } from "next/server";
import type { RenderHandle } from "@remocn/render-sdk";
import { sdk } from "@/lib/sdk";

export async function GET(
  _req: Request,
  { params }: { params: Promise<{ handle: string }> }
) {
  const { handle } = await params;
  const state = await sdk.getState(handle as RenderHandle);
  return NextResponse.json(state);
}

Returns { status, progress, error? }. progress is a number from 0 to 1.


Download route

// app/api/render/[handle]/download/route.ts
import type { RenderHandle } from "@remocn/render-sdk";
import { sdk } from "@/lib/sdk";

export async function GET(
  _req: Request,
  { params }: { params: Promise<{ handle: string }> }
) {
  const { handle } = await params;
  const stream = await sdk.download(handle as RenderHandle);

  return new Response(stream, {
    headers: {
      "Content-Type": "video/mp4",
      "Content-Disposition": 'attachment; filename="render.mp4"',
    },
  });
}

Only hit this route once status === "done". Calling it earlier throws RenderError("not_found") (server adapter) or fails to fetch the S3 URL (lambda adapter).


Client hook

"use client";

import { useState } from "react";

type Result = { status: string; progress: number; error?: string };

export function useRender() {
  const [state, setState] = useState<Result>({ status: "idle", progress: 0 });

  async function render(compositionId: string, inputProps: unknown) {
    const { handle } = await fetch("/api/render", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ compositionId, inputProps }),
    }).then((r) => r.json());

    while (true) {
      const next: Result = await fetch(`/api/render/${handle}`).then((r) =>
        r.json()
      );
      setState(next);

      if (next.status === "done") {
        window.location.href = `/api/render/${handle}/download`;
        break;
      }
      if (next.status === "error") {
        throw new Error(next.error ?? "Render failed");
      }

      await new Promise((r) => setTimeout(r, 1000));
    }
  }

  return { state, render };
}
"use client";

import { useRender } from "./use-render";

export function RenderButton() {
  const { state, render } = useRender();

  return (
    <button
      onClick={() => render("MyComp", { title: "Hello" })}
      disabled={state.status === "rendering" || state.status === "queued"}
    >
      {state.status === "rendering"
        ? `Rendering ${Math.round(state.progress * 100)}%`
        : "Render"}
    </button>
  );
}

Notes

  • Prefer redirecting to the file (window.location.href) over downloading a blob — it streams without buffering the whole video in memory.
  • With the Lambda adapter, return await sdk.getUrl(handle) from the poll route once done and redirect the browser straight to the S3 URL instead of proxying bytes through your server.
  • Route handlers are not for long-lived connections. Poll from the client (as above) rather than holding the request open.

On this page