Integrate file uploads into your Next.js app in minutes.
Install the SDK and React components in your Next.js project:
bun add @uploadx-sdk/core @uploadx-sdk/reactOr with npm / pnpm:
npm install @uploadx-sdk/core @uploadx-sdk/reactCreate a .env.local file with your API token and the dashboard URL. You can generate a token from the Tokens page of your app.
UPLOADX_TOKEN=upx_live_your_token_here
UPLOADX_URL=https://uploadx.crafter.runThat's it — no MinIO or storage configuration needed. The SDK automatically fetches connection details from the dashboard.
A File Router declares what files your app accepts. Each route specifies file types, size limits, and what happens after upload.
import { createUploadx } from "@uploadx-sdk/core/server";
import type { FileRouter } from "@uploadx-sdk/core/server";
const f = createUploadx();
export const fileRouter = {
// Accept up to 5 images, max 4MB each
imageUploader: f({ image: { maxFileSize: "4MB", maxFileCount: 5 } })
.middleware(({ req }) => {
// Run server-side logic (auth, etc.)
// Return metadata accessible in onUploadComplete
return { userId: "user_123" };
})
.onUploadComplete(({ metadata, file }) => {
console.log("Upload by", metadata.userId, ":", file.name);
return { uploadedBy: metadata.userId };
}),
// Accept any file type up to 16MB
fileUploader: f({ blob: { maxFileSize: "16MB" } })
.onUploadComplete(({ file }) => {
console.log("File uploaded:", file.name, file.size);
}),
} satisfies FileRouter;
export type AppFileRouter = typeof fileRouter;File types: image, video, audio, pdf, text, blob (any). Size limits: "4MB", "512KB", "1GB".
Expose the file router as a Next.js API route. This handles presigned URL generation and upload completion.
import { createNextRouteHandler } from "@uploadx-sdk/core/next";
import { fileRouter } from "@/lib/uploadx";
export const { GET, POST } = createNextRouteHandler({
router: fileRouter,
});Serve uploaded files through permanent, public URLs — no expiring presigned links. Mount a catch-all route that streams files directly from storage:
import { createNextFileServeHandler } from "@uploadx-sdk/core/next";
export const { GET } = createNextFileServeHandler();Files are now accessible at stable URLs that never expire:
// Use in <img> tags, links, or anywhere you need a permanent URL
<img src="/api/uploadx/f/abc123-photo.png" />
// Or build the URL from a file key
const publicUrl = `/api/uploadx/f/${file.key}`;The handler sets proper Content-Type, ETag, and Cache-Control headers automatically. Images display inline, PDFs render in the browser, and other files are served with their original content type.
Generate type-safe upload components bound to your file router:
import { generateUploadButton, generateUploadDropzone } from "@uploadx-sdk/react";
import type { AppFileRouter } from "./uploadx";
export const UploadButton = generateUploadButton<AppFileRouter>();
export const UploadDropzone = generateUploadDropzone<AppFileRouter>();Then use them in any client component:
"use client";
import { UploadDropzone } from "@/lib/uploadx-components";
export default function Home() {
return (
<UploadDropzone
endpoint="imageUploader"
onClientUploadComplete={(files) => {
console.log("Uploaded:", files);
}}
onUploadError={(error) => {
console.error("Error:", error);
}}
/>
);
}UploadButton renders a simple button with a hidden file input. UploadDropzone renders a drag-and-drop zone with a progress bar.
For full control, use the hook directly instead of the pre-built components:
"use client";
import { useUploadX } from "@uploadx-sdk/react";
import type { AppFileRouter } from "@/lib/uploadx";
export function CustomUploader() {
const { startUpload, isUploading, progress } = useUploadX<AppFileRouter>({
endpoint: "imageUploader",
onClientUploadComplete: (files) => {
console.log("Done:", files);
},
});
return (
<div>
<input
type="file"
onChange={async (e) => {
const files = Array.from(e.target.files ?? []);
await startUpload(files);
}}
/>
{isUploading && <p>Uploading... {progress}%</p>}
</div>
);
}The hook returns: startUpload, isUploading, progress (0-100), and routeConfig (file type constraints).
Use UploadxAPI on the server to list, delete, or generate download URLs for files:
import { UploadxAPI } from "@uploadx-sdk/core/server";
// Create instance (auto-fetches config from dashboard via token)
const api = await UploadxAPI.create();
// List all files in the bucket
const files = await api.listFiles();
// Generate a signed download URL (valid 1 hour)
const url = await api.generateSignedURL("file-key", 3600);
// Delete files
await api.deleteFiles(["file-key-1", "file-key-2"]);Example: create an API route for file management in your app:
import { NextResponse } from "next/server";
import { UploadxAPI } from "@uploadx-sdk/core/server";
let api: UploadxAPI | null = null;
async function getApi() {
if (!api) api = await UploadxAPI.create();
return api;
}
export async function GET() {
const files = await (await getApi()).listFiles();
return NextResponse.json(files);
}
export async function DELETE(request: Request) {
const { keys } = await request.json();
await (await getApi()).deleteFiles(keys);
return NextResponse.json({ ok: true });
}Everything the SDK does goes through a plain HTTP API you can call yourself — from a backend, a script, or any language without an UploadX SDK. Authenticate with an API token as a Bearer header:
# List files for the app the token belongs to
curl https://uploadx.crafter.run/api/files \
-H "Authorization: Bearer $UPLOADX_TOKEN"
# Delete files by storage key
curl -X DELETE https://uploadx.crafter.run/api/files \
-H "Authorization: Bearer $UPLOADX_TOKEN" \
-H "Content-Type: application/json" \
-d '{"keys":["1712345678901-photo.png"]}'The full reference — every endpoint, schema, and response, with a built-in request playground — lives at /docs/api. The OpenAPI document itself is served from /api/openapi if you want to generate a client from it.
Open the API ReferenceEverything in this dashboard can also be driven from a terminal. Install the CLI globally — the command is uploadx:
npm install -g @uploadx-sdk/cliOr run it without installing:
npx @uploadx-sdk/cli --helpSign in once per machine. The CLI prints a code, you approve it in a browser — the browser does not have to be on the same machine, so this works over SSH:
uploadx login
# Point a project at an app, then upload
uploadx init
uploadx files upload ./dist --recursive --prefix build/Day-to-day commands:
uploadx apps list # every app in your org
uploadx tokens create "CI" --output-env # mint a token straight into .env.local
uploadx files list --search logo --json # --json on any read command
uploadx files delete <key> --yesIn CI there is no browser, so set UPLOADX_TOKEN to an app token instead — the same one the SDK uses. That covers the files commands.
If you work with Claude Code, Cursor or another agent, install the UploadX skill. It teaches the agent the commands, the two authentication modes, and the fact that uploadx login needs a human — so it asks you instead of hanging on a device code:
npx skills@latest add crafter-station/uploadx --skill=uploadxAdd --global to install it for every project rather than just the current one.