Process uploads in Next.js with Ittybit
Users upload a file through a form. A Server Action stores it and dispatches a processing task to Ittybit. When the task finishes, Ittybit POSTs a webhook to your Route Handler so you can update your database.
Create a task
Once the file is stored (S3, Vercel Blob, wherever), POST the URL to Ittybit:
const res = await fetch("https://api.ittybit.com/jobs", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.ITTYBIT_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
input: "https://your-bucket.s3.amazonaws.com/uploads/video.mov",
kind: "video",
options: {
width: 1280,
format: "mp4",
quality: "high",
},
}),
});
const { id, status } = await res.json();
// id: "task_abc123", status: "pending"curl -X POST https://api.ittybit.com/jobs \
-H "Authorization: Bearer $ITTYBIT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"input": "https://your-bucket.s3.amazonaws.com/uploads/video.mov",
"kind": "video",
"options": {
"width": 1280,
"format": "mp4",
"quality": "high"
}
}' The response includes a task id and status: "pending". Ittybit will POST to your configured webhook URL when processing completes.
Server Action
Handle the form submission, upload the file, and create the Ittybit task in a single action.
// app/actions.ts
'use server';
import { put } from '@vercel/blob';
import { revalidatePath } from 'next/cache';
import { db } from '@/lib/db';
export async function uploadMedia(formData: FormData) {
const file = formData.get('file') as File;
if (!file) throw new Error('No file provided');
// 1. Store the file
const blob = await put(file.name, file, { access: 'public' });
// 2. Save a record in your database
const record = await db.media.create({
data: {
filename: file.name,
sourceUrl: blob.url,
status: 'processing',
},
});
// 3. Dispatch to Ittybit
const res = await fetch('https://api.ittybit.com/jobs', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.ITTYBIT_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
input: blob.url,
kind: 'video',
options: { width: 1280, format: 'mp4', quality: 'high' },
metadata: { media_id: record.id },
}),
});
if (!res.ok) {
throw new Error('Failed to create task');
}
revalidatePath('/uploads');
}
The metadata field is passed through to the webhook payload, so you can tie the callback back to your database record.
Webhook Route Handler
When Ittybit finishes processing, it POSTs to your webhook endpoint. Create a Route Handler to receive it.
// app/api/webhooks/ittybit/route.ts
import { NextResponse } from 'next/server';
import { db } from '@/lib/db';
export async function POST(request: Request) {
const body = await request.json();
const { id, status, kind, output, metadata } = body;
if (status === 'completed' && metadata?.media_id) {
await db.media.update({
where: { id: metadata.media_id },
data: {
status: 'ready',
outputUrl: output?.url,
},
});
}
if (status === 'failed' && metadata?.media_id) {
await db.media.update({
where: { id: metadata.media_id },
data: { status: 'failed' },
});
}
return NextResponse.json({ received: true });
}
Upload form
Wire the Server Action to a form component:
// app/uploads/page.tsx
import { uploadMedia } from '@/app/actions';
export default function UploadPage() {
return (
<form action={uploadMedia}>
<input type="file" name="file" accept="video/*" required />
<button type="submit">Upload</button>
</form>
);
}