Quickstart
A working S3 upload in minutes — new app or existing project.
Create a new project
Scaffold a full-stack project with preconfigured S3 routes and UI:
npx @dimah-s3/cli@latest createManual installation
Install dependencies
npm i @dimah-s3/server @dimah-s3/react @aws-sdk/client-s3Configure S3 and routes
S3_ENDPOINT="https://your-endpoint.example.com"
S3_REGION="auto"
S3_ACCESS_KEY_ID="your-access-key-id"
S3_SECRET_ACCESS_KEY="your-secret-access-key"
S3_BUCKET="your-bucket-name"import { S3Client } from "@aws-sdk/client-s3";
import { dimahS3, route } from "@dimah-s3/server";
export const awsS3 = new S3Client({
region: process.env.S3_REGION,
endpoint: process.env.S3_ENDPOINT,
credentials: {
accessKeyId: process.env.S3_ACCESS_KEY_ID!,
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY!,
},
});
export const s3 = dimahS3({
client: awsS3,
bucket: process.env.S3_BUCKET!,
routes: {
avatar: route({
upload: {
fileTypes: ["image/*"],
maxFileSize: 2 * 1024 * 1024, // 2MB
},
download: true,
}),
},
});Set bucket CORS for your origin — Amazon S3, Cloudflare R2, or MinIO.
Cloudflare R2
Set upload.method to "PUT". See Cloudflare
R2.
Mount the route handler
import { toNextJsHandler } from "@dimah-s3/server/next";
import { s3 } from "@/lib/s3";
export const { GET, POST, PUT, PATCH, DELETE } = toNextJsHandler(s3);Cache Components
Next.js 16.4 enables Cache Components on new apps. This handler reads the request, so it runs on each request. Keep the handler as written. Server Setup explains why it must stay uncached.
Other runtimes: Server Setup.
Create the client and provider
"use client";
import { createS3Client } from "@dimah-s3/react";
export const s3Client = createS3Client();
export const S3Provider = s3Client.Provider;Wrap your application
npm i @dimah-s3/ui shadcn@import "shadcn/tailwind.css";
@import "@dimah-s3/ui/styles.css";
/* shadcn theme variables (`--primary`, `--muted`, …) */
import { Toaster } from "@dimah-s3/ui";
import { S3Provider } from "@/lib/s3-client";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<S3Provider>{children}</S3Provider>
<Toaster />
</body>
</html>
);
}Add an upload button
"use client";
import { useUpload } from "@dimah-s3/react";
import { UploadButton } from "@dimah-s3/ui";
export default function Page() {
const upload = useUpload({ route: "avatar" });
return <UploadButton upload={upload} />;
}Selecting a file presigns a secure URL and uploads directly to your bucket.
Frequently asked questions
Add a guard on the route or instance. Throw errors.unauthorized() or errors.forbidden() if the session is invalid:
import { dimahS3, errors, route } from "@dimah-s3/server";
export const s3 = dimahS3({
// ...
routes: {
avatar: route({
guard: async ({ request }) => {
const session = await getSession(request);
if (!session) throw errors.unauthorized();
},
upload: {
fileTypes: ["image/*"],
maxFileSize: 2 * 1024 * 1024,
},
}),
},
});Persist trusted object metadata in onConfirmed, after the server has verified
the stored object with HeadObject:
avatar: route({
upload: {
fileTypes: ["image/*"],
maxFileSize: 2 * 1024 * 1024,
onConfirmed: async ({ key, request }) => {
const session = await getSession(request);
await db.user.update({
where: { id: session.userId },
data: { avatarKey: key },
});
},
},
});For a form, onSuccess can place the confirmed key in local form state before
your application submits it to its own API:
const upload = useUpload({
route: "avatar",
onSuccess: (results) => {
setValue("avatarKey", results[0]?.key);
},
});Cloudflare R2 requires upload: { method: "PUT" } because it does not support presigned POST:
export const s3 = dimahS3({
client: r2Client,
bucket: process.env.R2_BUCKET!,
routes: {
avatar: route({
upload: {
method: "PUT",
fileTypes: ["image/*"],
maxFileSize: 2 * 1024 * 1024,
},
}),
},
});See the Cloudflare R2 guide for full details.
Use useObjectUrl to generate a temporary display URL, or construct your CDN URL if the bucket is public:
"use client";
import { useObjectUrl } from "@dimah-s3/react";
export function AvatarPreview({ avatarKey }: { avatarKey: string }) {
const { url } = useObjectUrl({
route: "avatar",
objectKey: avatarKey,
disposition: "inline",
});
if (!url) return null;
return (
<img
src={url}
alt="User avatar"
className="size-16 rounded-full object-cover"
/>
);
}