Setting Up Rendering
How to build the API endpoints your editor needs to export video
The RVE SDK handles everything in the browser — editing, previewing, timeline management. But when a user clicks Export, the SDK needs a server to actually render the video file.
You provide this by implementing two API endpoints. The SDK's HttpRenderer calls them automatically.
How It Works
User clicks "Export"
│
▼
SDK calls POST /api/render/ssr/render
→ Your server starts a Remotion render job
→ Returns { renderId: "uuid" }
│
▼
SDK polls POST /api/render/ssr/progress
→ Your server returns progress, done, or error
→ SDK shows progress bar to user
│
▼
Render complete → SDK receives download URLThe API Contract
Your server needs to implement two endpoints. The base path is whatever you pass to HttpRenderer:
// If you configure this:
new HttpRenderer('/api/render/ssr', { type: 'ssr', entryPoint: '/api/render/ssr' })
// The SDK will call:
// POST /api/render/ssr/render
// POST /api/render/ssr/progressPOST {base}/render
Request body:
{
"id": "composition-id",
"inputProps": {
"overlays": [...],
"durationInFrames": 900,
"width": 1920,
"height": 1080,
"fps": 30
}
}Expected response:
{
"renderId": "unique-render-id"
}POST {base}/progress
Request body:
{
"id": "the-renderId-from-above"
}Expected response (one of three shapes):
{
"type": "progress",
"progress": 0.45
}{
"type": "done",
"url": "/rendered-videos/abc123.mp4",
"size": 12345678
}{
"type": "error",
"message": "Render failed: out of memory"
}The SDK polls the progress endpoint every second until it receives a done or error response.
Next.js Implementation
Here's a working implementation using Next.js App Router and Remotion SSR.
Install Dependencies
npm install @remotion/bundler @remotion/renderer remotion uuidRender State Helper
A simple file-based state store so the render and progress endpoints can share state:
import fs from 'fs';
import path from 'path';
const RENDER_STATE_DIR = path.join(process.cwd(), 'tmp', 'render-state');
if (!fs.existsSync(RENDER_STATE_DIR)) {
fs.mkdirSync(RENDER_STATE_DIR, { recursive: true });
}
export const saveRenderState = (renderId: string, state: unknown) => {
fs.writeFileSync(
path.join(RENDER_STATE_DIR, `${renderId}.json`),
JSON.stringify(state)
);
};
export const getRenderState = (renderId: string) => {
const filePath = path.join(RENDER_STATE_DIR, `${renderId}.json`);
if (!fs.existsSync(filePath)) return null;
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
};
export const updateRenderProgress = (renderId: string, progress: number) => {
const state = getRenderState(renderId) || {};
state.progress = progress;
state.status = 'rendering';
saveRenderState(renderId, state);
};
export const completeRender = (renderId: string, url: string, size: number) => {
const state = getRenderState(renderId) || {};
state.status = 'done';
state.url = url;
state.size = size;
saveRenderState(renderId, state);
};
export const failRender = (renderId: string, error: string) => {
const state = getRenderState(renderId) || {};
state.status = 'error';
state.error = error;
saveRenderState(renderId, state);
};Render Endpoint
import { NextRequest, NextResponse } from 'next/server';
import { bundle } from '@remotion/bundler';
import { renderMedia, selectComposition } from '@remotion/renderer';
import { v4 as uuidv4 } from 'uuid';
import path from 'path';
import fs from 'fs';
import {
saveRenderState,
updateRenderProgress,
completeRender,
failRender,
} from '../lib/render-state';
const VIDEOS_DIR = path.join(process.cwd(), 'public', 'rendered-videos');
if (!fs.existsSync(VIDEOS_DIR)) {
fs.mkdirSync(VIDEOS_DIR, { recursive: true });
}
export async function POST(request: NextRequest) {
try {
const { id, inputProps } = await request.json();
const renderId = uuidv4();
// Save initial state
saveRenderState(renderId, {
status: 'rendering',
progress: 0,
timestamp: Date.now(),
});
// Start rendering asynchronously (don't await)
(async () => {
try {
// Bundle your Remotion composition
const bundleLocation = await bundle(
path.join(process.cwd(), 'remotion', 'index.ts')
);
// Select the composition
const composition = await selectComposition({
serveUrl: bundleLocation,
id,
inputProps,
});
// Render the video
await renderMedia({
codec: 'h264',
composition: {
...composition,
durationInFrames: inputProps.durationInFrames || composition.durationInFrames,
width: inputProps.width || composition.width,
height: inputProps.height || composition.height,
},
serveUrl: bundleLocation,
outputLocation: path.join(VIDEOS_DIR, `${renderId}.mp4`),
inputProps,
onProgress: ({ progress }) => {
updateRenderProgress(renderId, progress);
},
});
const stats = fs.statSync(path.join(VIDEOS_DIR, `${renderId}.mp4`));
completeRender(renderId, `/rendered-videos/${renderId}.mp4`, stats.size);
} catch (error) {
failRender(renderId, (error as Error).message);
}
})();
return NextResponse.json({ renderId });
} catch (error) {
return NextResponse.json(
{ error: 'Failed to start render' },
{ status: 500 }
);
}
}Progress Endpoint
import { NextRequest, NextResponse } from 'next/server';
import { getRenderState } from '../lib/render-state';
export async function POST(request: NextRequest) {
try {
const { id } = await request.json();
const renderState = getRenderState(id);
if (!renderState) {
return NextResponse.json({
type: 'error',
message: `No render found with ID: ${id}`,
});
}
switch (renderState.status) {
case 'error':
return NextResponse.json({
type: 'error',
message: renderState.error || 'Unknown error',
});
case 'done':
return NextResponse.json({
type: 'done',
url: renderState.url,
size: renderState.size,
});
default:
return NextResponse.json({
type: 'progress',
progress: renderState.progress || 0,
});
}
} catch (error) {
return NextResponse.json(
{ error: 'Failed to get render progress' },
{ status: 500 }
);
}
}Remotion Composition
Your render endpoint needs a Remotion composition that knows how to render the SDK's overlay data. The SDK exports layer components you can use in your composition:
import { registerRoot } from 'remotion';
import { MyComposition } from './Composition';
registerRoot(MyComposition);Remotion setup
Setting up the Remotion composition is covered in detail in the Remotion documentation. The SDK provides layer rendering components (text-layer-content, video-layer-content, image-layer-content, etc.) that you can use inside your composition to render each overlay type.
Rendering Modes
| SSR | Lambda | |
|---|---|---|
| Where it runs | Your server | AWS Lambda |
| Best for | Development, small scale | Production, high volume |
| Scaling | Manual | Automatic |
| Setup | Simple | Requires AWS configuration |
| Cost | Your server costs | Pay per render |
- SSR Rendering — render on your own server
- Lambda Rendering — render on AWS Lambda for production scale