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
Define your AWS S3 client and instance. In this example, we create an avatar route for user profile photos:
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
},
}),
},
});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);Create the client client & 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`, …) */
/* Optional: theme dimah-s3 independently of the rest of the app
@theme {
--color-dimah-s3-primary: oklch(0.65 0.15 150);
}
*/
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 AvatarUploadPage() {
const upload = useUpload({ route: "avatar" });
return <UploadButton upload={upload} />;
}Selecting an image presigns a secure URL and uploads directly to your bucket under avatar/{uuid}/{filename}.
Frequently asked questions
Add a guard on the route or instance. Throw errors.unauthorized() or errors.forbidden() if the session is invalid:
import { 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,
},
}),
},
});Handle onConfirmed on the server or onSuccess on the client:
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 },
});
},
},
});const upload = useUpload({
route: "avatar",
onSuccess: (results) => {
console.log("Uploaded avatar key:", 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"
/>
);
}