React
Set up Clawdy file uploads in a React project
Installation
Install the required packages:
npm install @clawdyup/core @clawdyup/reactDefine a File Router
Create a file router to configure which files your application accepts. This is the core of your upload configuration.
import { createClawdy, type FileRouter } from "@clawdyup/core";
const f = createClawdy();
export const ourFileRouter = {
imageUploader: f({ image: { maxFileSize: "4MB" } })
.middleware(async ({ req }) => {
// Run server-side logic before upload, e.g. auth checks
return { userId: "user_123" };
})
.onUploadComplete(async ({ metadata, file }) => {
console.log("Upload complete for user:", metadata.userId);
console.log("File URL:", file.url);
}),
bannerUploader: f({ image: { maxFileSize: "10MB" } })
.middleware(async ({ req }) => {
return {};
})
.onUploadComplete(async ({ metadata, file }) => {
console.log("Banner uploaded:", file.url);
}),
fileUploader: f({ blob: { maxFileSize: "50MB" } })
.middleware(async ({ req }) => {
return {};
})
.onUploadComplete(async ({ metadata, file }) => {
console.log("File uploaded:", file.url);
}),
} satisfies FileRouter;
export type OurFileRouter = typeof ourFileRouter;Create a Route Handler
Expose the file router as an API route so the client can communicate with it.
import { createRouteHandler } from "@clawdyup/core/server";
import { ourFileRouter } from "./core";
export const { GET, POST } = createRouteHandler({
router: ourFileRouter,
config: {
apiUrl: process.env.CLAWDY_API_URL || "http://127.0.0.1:3030",
apiKey: process.env.CLAWDY_API_KEY,
},
});Next.js Configuration
If you're using a local or symlinked version of @clawdyup/core (e.g. during development with file: dependencies), add transpilePackages to your next.config.mjs:
const nextConfig = {
transpilePackages: ["@clawdyup/core", "@clawdyup/react"],
// ...rest of your config
};This ensures Next.js (especially Turbopack) correctly resolves subpath imports like @clawdyup/core/client and @clawdyup/core/server from symlinked packages. This is only needed when using file: dependencies — npm installs work without it.
Environment Variables
Add the following environment variables to your .env.local file:
CLAWDY_API_URL=https://your-clawdy-server.example.com
CLAWDY_API_KEY=your_api_key_hereCLAWDY_API_URL-- The URL of your Clawdy server.CLAWDY_API_KEY-- Your API key for authenticating with the Clawdy server.
Upload Components
Clawdy provides three ways to add uploads to your app. Generate typed components bound to your file router, then pick the approach that fits your needs.
import {
generateUploadButton,
generateUploadDropzone,
generateReactHelpers,
} from "@clawdyup/react";
import type { OurFileRouter } from "@/app/api/clawdy/core";
export const UploadButton = generateUploadButton<OurFileRouter>();
export const UploadDropzone = generateUploadDropzone<OurFileRouter>();
export const { useClawdy } = generateReactHelpers<OurFileRouter>();1. UploadButton
A simple button that opens the native file picker. Best for inline upload actions.
"use client";
import { useState } from "react";
import { UploadButton } from "@/lib/clawdy";
export function ImageUpload() {
const [imageUrl, setImageUrl] = useState<string | null>(null);
return (
<div>
<UploadButton
endpoint="imageUploader"
onClientUploadComplete={(res) => {
// res is an array of uploaded files
const file = res[0];
setImageUrl(file.url);
console.log("Uploaded:", file.name, file.url);
}}
onUploadError={(error) => {
alert(`Upload failed: ${error.message}`);
}}
onUploadProgress={(progress) => {
console.log(`Progress: ${progress}%`);
}}
onUploadBegin={(fileName) => {
console.log("Starting upload:", fileName);
}}
/>
{imageUrl && (
<img
src={imageUrl}
alt="Uploaded image"
style={{ maxWidth: 400, marginTop: 16, borderRadius: 8 }}
/>
)}
</div>
);
}Props:
| Prop | Type | Description |
|---|---|---|
endpoint | string | The file router endpoint to use (type-safe) |
input | object | Optional metadata passed to middleware |
onClientUploadComplete | (res) => void | Called with uploaded file info on success |
onUploadError | (error) => void | Called when upload fails |
onUploadProgress | (progress) => void | Called with progress percentage (0-100) |
onUploadBegin | (fileName) => void | Called when upload starts |
onBeforeUploadBegin | (files) => File[] | Transform or filter files before upload |
disabled | boolean | Disable the button |
className | string | Custom CSS class |
2. UploadDropzone
A drag-and-drop zone for file uploads. Best for dedicated upload areas.
"use client";
import { useState } from "react";
import { UploadDropzone } from "@/lib/clawdy";
export function FileDropzone() {
const [uploadedFiles, setUploadedFiles] = useState<
{ name: string; url: string; size: number }[]
>([]);
const [progress, setProgress] = useState(0);
return (
<div>
<UploadDropzone
endpoint="fileUploader"
onClientUploadComplete={(res) => {
setUploadedFiles((prev) => [
...prev,
...res.map((f) => ({ name: f.name, url: f.url, size: f.size })),
]);
setProgress(0);
}}
onUploadError={(error) => {
alert(`Upload failed: ${error.message}`);
setProgress(0);
}}
onUploadProgress={(pct) => setProgress(pct)}
/>
{/* Progress bar */}
{progress > 0 && progress < 100 && (
<div style={{ marginTop: 12 }}>
<div
style={{
height: 6,
borderRadius: 3,
backgroundColor: "#e5e7eb",
overflow: "hidden",
}}
>
<div
style={{
width: `${progress}%`,
height: "100%",
backgroundColor: "#3b82f6",
transition: "width 0.3s",
}}
/>
</div>
<p style={{ fontSize: 12, color: "#6b7280", marginTop: 4 }}>
{progress}%
</p>
</div>
)}
{/* Display uploaded files as images */}
{uploadedFiles.length > 0 && (
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fill, minmax(150px, 1fr))",
gap: 12,
marginTop: 16,
}}
>
{uploadedFiles.map((file, i) => (
<div key={i}>
<img
src={file.url}
alt={file.name}
style={{
width: "100%",
aspectRatio: "1",
objectFit: "cover",
borderRadius: 8,
}}
/>
<p style={{ fontSize: 12, marginTop: 4 }}>{file.name}</p>
</div>
))}
</div>
)}
</div>
);
}The UploadDropzone accepts the same props as UploadButton. Users can click the zone or drag files onto it.
3. useClawdy Hook
Full control over the upload UI. Build your own components while Clawdy handles the upload logic.
"use client";
import { useState, useRef } from "react";
import { useClawdy } from "@/lib/clawdy";
export function CustomUploader() {
const [images, setImages] = useState<{ url: string; name: string }[]>([]);
const [progress, setProgress] = useState(0);
const inputRef = useRef<HTMLInputElement>(null);
const { startUpload, isUploading } = useClawdy("bannerUploader", {
onClientUploadComplete: (res) => {
setImages((prev) => [
...prev,
...res.map((f) => ({ url: f.url, name: f.name })),
]);
setProgress(0);
},
onUploadError: (error) => {
console.error("Upload failed:", error.message);
setProgress(0);
},
onUploadProgress: (pct) => setProgress(pct),
});
const handleFileSelect = async (e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files;
if (!files || files.length === 0) return;
await startUpload(Array.from(files));
e.target.value = "";
};
return (
<div>
{/* Custom upload trigger */}
<input
ref={inputRef}
type="file"
accept="image/*"
onChange={handleFileSelect}
style={{ display: "none" }}
disabled={isUploading}
/>
<button
onClick={() => inputRef.current?.click()}
disabled={isUploading}
style={{
padding: "10px 20px",
borderRadius: 8,
border: "none",
backgroundColor: isUploading ? "#9ca3af" : "#ef4444",
color: "white",
cursor: isUploading ? "not-allowed" : "pointer",
fontSize: 14,
fontWeight: 600,
}}
>
{isUploading ? `Uploading... ${progress}%` : "Upload Banner Image"}
</button>
{/* Progress bar */}
{isUploading && (
<div
style={{
marginTop: 8,
height: 4,
borderRadius: 2,
backgroundColor: "#e5e7eb",
overflow: "hidden",
}}
>
<div
style={{
width: `${progress}%`,
height: "100%",
backgroundColor: "#ef4444",
transition: "width 0.2s",
}}
/>
</div>
)}
{/* Render uploaded images */}
{images.length > 0 && (
<div style={{ marginTop: 16 }}>
{images.map((img, i) => (
<div key={i} style={{ marginBottom: 12 }}>
<img
src={img.url}
alt={img.name}
style={{
maxWidth: "100%",
borderRadius: 8,
}}
/>
<p style={{ fontSize: 12, color: "#6b7280", marginTop: 4 }}>
{img.name}
</p>
</div>
))}
</div>
)}
</div>
);
}Return values from useClawdy:
| Value | Type | Description |
|---|---|---|
startUpload | (files, opts?) => Promise | Trigger upload with files and optional input |
isUploading | boolean | Whether an upload is in progress |
Options:
| Option | Type | Description |
|---|---|---|
onClientUploadComplete | (res) => void | Success callback with uploaded file info |
onUploadError | (error) => void | Error callback |
onUploadProgress | (progress) => void | Progress percentage (0-100) |
onUploadBegin | (fileName) => void | Called when upload starts |
Upload Response
All three approaches return the same response shape on success:
interface UploadFileResponse {
key: string; // Unique file ID
url: string; // Public URL to access the file
name: string; // Original filename
size: number; // File size in bytes
serverData: unknown; // Custom data from onUploadComplete
}Listing & Retrieving Files
Use listFiles and getFile from @clawdyup/core/client to fetch files uploaded by the current API key.
List all files
"use client";
import { useState, useEffect } from "react";
import { listFiles } from "@clawdyup/core/client";
import type { ListedFile } from "@clawdyup/core/client";
export function FileGallery() {
const [files, setFiles] = useState<ListedFile[]>([]);
const [hasMore, setHasMore] = useState(false);
useEffect(() => {
loadFiles(0);
}, []);
async function loadFiles(offset: number) {
const res = await listFiles({ limit: 20, offset });
setFiles((prev) => (offset === 0 ? res.files : [...prev, ...res.files]));
setHasMore(res.hasMore);
}
return (
<div>
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(150px, 1fr))", gap: 12 }}>
{files.map((file) => (
<div key={file.id}>
{file.mimeType.startsWith("image/") ? (
<img src={file.url} alt={file.filename} style={{ width: "100%", aspectRatio: "1", objectFit: "cover", borderRadius: 8 }} />
) : (
<div style={{ width: "100%", aspectRatio: "1", display: "flex", alignItems: "center", justifyContent: "center", background: "#f3f4f6", borderRadius: 8 }}>
{file.mimeType.startsWith("video/") ? "Video" : "File"}
</div>
)}
<p style={{ fontSize: 12, marginTop: 4 }}>{file.filename}</p>
</div>
))}
</div>
{hasMore && (
<button onClick={() => loadFiles(files.length)}>Load More</button>
)}
</div>
);
}Options for listFiles:
| Option | Type | Default | Description |
|---|---|---|---|
url | string | "/api/clawdy" | API route URL |
limit | number | 50 | Max files per page (max 500) |
offset | number | 0 | Pagination offset |
category | string | — | Filter by category |
tag | string | — | Filter by tags (comma-separated) |
Get a single file by ID
"use client";
import { useState } from "react";
import { getFile } from "@clawdyup/core/client";
import type { FileDetail } from "@clawdyup/core/client";
export function FileViewer({ fileId }: { fileId: string }) {
const [file, setFile] = useState<FileDetail | null>(null);
useEffect(() => {
getFile(fileId).then(setFile);
}, [fileId]);
if (!file) return <p>Loading...</p>;
return (
<div>
{file.mimeType.startsWith("image/") && (
<img src={file.url} alt={file.filename} style={{ maxWidth: "100%", borderRadius: 8 }} />
)}
{file.mimeType.startsWith("video/") && (
<video src={file.url} controls style={{ maxWidth: "100%", borderRadius: 8 }} />
)}
<h2>{file.filename}</h2>
<p>Size: {file.size} bytes</p>
<p>Type: {file.mimeType}</p>
<p>Uploaded: {new Date(file.createdAt).toLocaleDateString()}</p>
{file.category && <p>Category: {file.category}</p>}
{file.tags && <p>Tags: {file.tags.join(", ")}</p>}
</div>
);
}FileDetail shape:
interface FileDetail {
id: string;
filename: string;
url: string;
mimeType: string;
size: number;
contentHash: string;
createdAt: string;
category?: string;
tags?: string[];
isChunked: boolean;
}