# Quickstart (https://v2.s3.dimah.dev/docs/quickstart)

## Create a new project [#create-a-new-project]

Scaffold a full-stack project with preconfigured S3 routes and UI. `create`
also writes a generated `DIMAH_S3_SECRET` to `.env`.

```npm
npx @dimah-s3/cli@beta create
```

---

## Manual installation [#manual-installation]

### Install dependencies [step] [#install-dependencies-step]

```npm
npm i @dimah-s3/server@beta @dimah-s3/core@beta @dimah-s3/react@beta @fuma-translate/react @aws-sdk/client-s3
```

`@dimah-s3/react` needs React 19.2 or later.

### Configure S3 and routes [step] [#configure-s3-and-routes-step]

```dotenv title=".env"
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"

# Signs upload tickets. Generate one with: openssl rand -base64 32
DIMAH_S3_SECRET="your-generated-secret"
```

```ts title="lib/s3.ts"
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!,
  },
  // The SDK default (WHEN_SUPPORTED) breaks presigned uploads.
  requestChecksumCalculation: "WHEN_REQUIRED",
});

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](https://v2.s3.dimah.dev/docs/providers/aws-s3),
[Cloudflare R2](https://v2.s3.dimah.dev/docs/providers/cloudflare-r2), or [MinIO](https://v2.s3.dimah.dev/docs/providers/minio).

<Callout title="Upload secret">
  `dimahS3()` reads `DIMAH_S3_SECRET` to sign [upload
  tickets](https://v2.s3.dimah.dev/docs/server/upload-tickets). Use at least 32 characters and the same
  value on every server instance, or pass `secret` yourself. When it is missing,
  `development` and `test` fall back to a random secret with a warning; anywhere
  else, uploads return `500`.
</Callout>

### Mount the route handler [step] [#mount-the-route-handler-step]

<Tabs items={["Next.js", "Vite", "Express", "Hono", "SvelteKit"]}>

<Tab value="Next.js">

```ts title="app/api/s3/[...s3]/route.ts"
import { toNextJsHandler } from "@dimah-s3/server/next";
import { s3 } from "@/lib/s3";

export const { GET, POST, PUT, PATCH, DELETE } = toNextJsHandler(s3);
```

<Callout title="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](https://v2.s3.dimah.dev/docs/server/setup) explains why it must stay uncached.
</Callout>

</Tab>

<Tab value="Vite">

```ts title="server/index.ts"
import { Hono } from "hono";
import { toHonoHandler } from "@dimah-s3/server/hono";
import { s3 } from "./s3";

const app = new Hono();
app.on(
  ["GET", "POST", "PUT", "PATCH", "DELETE"],
  "/api/s3/*",
  toHonoHandler(s3),
);
```

</Tab>

<Tab value="Express">

```ts title="server.ts"
import express from "express";
import { toExpressHandler } from "@dimah-s3/server/express";
import { s3 } from "./s3";

const app = express();
app.all("/api/s3/*", toExpressHandler(s3));
app.use(express.json());
```

</Tab>

<Tab value="Hono">

```ts title="src/index.ts"
import { Hono } from "hono";
import { toHonoHandler } from "@dimah-s3/server/hono";
import { s3 } from "./s3";

const app = new Hono();
app.on(
  ["GET", "POST", "PUT", "PATCH", "DELETE"],
  "/api/s3/*",
  toHonoHandler(s3),
);
```

</Tab>

<Tab value="SvelteKit">

```ts title="src/routes/api/s3/[...path]/+server.ts"
import { toSvelteKitHandler } from "@dimah-s3/server/svelte-kit";
import { s3 } from "$lib/s3";

const handler = toSvelteKitHandler(s3);
export const GET = handler;
export const POST = handler;
export const PUT = handler;
export const PATCH = handler;
export const DELETE = handler;
```

</Tab>

</Tabs>

Other runtimes: [Server Setup](https://v2.s3.dimah.dev/docs/server/setup).

### Create the client and provider [step] [#create-the-client-and-provider-step]

```ts title="lib/s3-client.ts"
"use client";

import { createS3Client } from "@dimah-s3/react";

export const s3Client = createS3Client();
export const S3Provider = s3Client.Provider;
```

### Wrap your application [step] [#wrap-your-application-step]

<Tabs items={["npm package", "shadcn registry"]}>

<Tab value="npm package">

```npm
npm i @dimah-s3/ui@beta shadcn
```

```css title="app/globals.css"
@import "shadcn/tailwind.css";
@import "@dimah-s3/ui/styles.css"; /* [!code ++] */

/* shadcn theme variables (`--primary`, `--muted`, …) */
```

```tsx title="app/layout.tsx"
import { Toaster } from "@dimah-s3/ui"; // [!code ++]
import { S3Provider } from "@/lib/s3-client"; // [!code ++]

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        {/* [!code ++] */}
        <S3Provider>{children}</S3Provider>
        {/* [!code ++] */}
        <Toaster />
      </body>
    </html>
  );
}
```

</Tab>

<Tab value="shadcn registry">

```json title="components.json"
{
  "registries": {
    "@dimah-s3": "https://v2.s3.dimah.dev/r/{name}.json" // [!code ++]
  }
}
```

```npm
npx shadcn@latest add @dimah-s3/upload
```

```tsx title="app/layout.tsx"
import { Toaster } from "@/components/ui/toast"; // [!code ++]
import { S3Provider } from "@/lib/s3-client"; // [!code ++]

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        {/* [!code ++] */}
        <S3Provider>{children}</S3Provider>
        {/* [!code ++] */}
        <Toaster />
      </body>
    </html>
  );
}
```

</Tab>

</Tabs>

### Add an upload button [step] [#add-an-upload-button-step]

```tsx title="app/page.tsx"
"use client";

import { useUpload } from "@dimah-s3/react";
import { Upload, UploadToasts, UploadTrigger } from "@dimah-s3/ui";

export default function Page() {
  const upload = useUpload({ route: "avatar" });

  return (
    <>
      <Upload upload={upload}>
        <UploadTrigger />
      </Upload>
      <UploadToasts upload={upload} />
    </>
  );
}
```

Selecting a file presigns a secure URL and uploads directly to your bucket.
`UploadToasts` reports progress, success, and errors for this hook. See
[Upload](https://v2.s3.dimah.dev/docs/react/ui/components/upload) for the dropzone and status parts.

## Frequently asked questions [#frequently-asked-questions]

<Accordions>

<Accordion title="How do I restrict uploads to authenticated users?">

Add a `guard` on the route or instance. Throw `s3Error("UNAUTHORIZED")` or `s3Error("FORBIDDEN")` when the session is missing or not allowed:

```ts title="lib/s3.ts"
import { dimahS3, route, s3Error } from "@dimah-s3/server";

export const s3 = dimahS3({
  // ...
  routes: {
    avatar: route({
      guard: async ({ request }) => {
        // [!code ++]
        const session = await getSession(request); // [!code ++]
        if (!session) throw s3Error("UNAUTHORIZED"); // [!code ++]
      }, // [!code ++]
      upload: {
        fileTypes: ["image/*"],
        maxFileSize: 2 * 1024 * 1024,
      },
    }),
  },
});
```

</Accordion>

<Accordion title="How do I save the uploaded avatar key to my database?">

Persist trusted object metadata in `onConfirmed`, after the server has verified
the stored object with `HeadObject`. It can run more than once for one upload,
so make the write idempotent:

```ts title="Server-side (recommended)"
avatar: route({
  upload: {
    fileTypes: ["image/*"],
    maxFileSize: 2 * 1024 * 1024,
    onConfirmed: async ({ key, request }) => {
      // [!code ++]
      const session = await getSession(request); // [!code ++]
      await db.user.update({
        // [!code ++]
        where: { id: session.userId }, // [!code ++]
        data: { avatarKey: key }, // [!code ++]
      }); // [!code ++]
    }, // [!code ++]
  },
});
```

For a form, `onSuccess` runs once per confirmed file and can place its key in
local form state before your application submits it to its own API:

```tsx title="Client-side form state"
const upload = useUpload({
  route: "avatar",
  // [!code ++]
  onSuccess: (result) => {
    setValue("avatarKey", result.key); // [!code ++]
  }, // [!code ++]
});
```

</Accordion>

<Accordion title="How do I use Cloudflare R2 instead of AWS S3?">

Pass an `S3Client` for your R2 account. Uploads use presigned PUT by default,
which R2 supports. Do not set `method: "POST"`: R2 has no presigned POST.

```ts title="lib/s3.ts"
export const s3 = dimahS3({
  client: r2Client,
  bucket: process.env.R2_BUCKET!,
  routes: {
    avatar: route({
      upload: {
        fileTypes: ["image/*"],
        maxFileSize: 2 * 1024 * 1024,
      },
    }),
  },
});
```

See the [Cloudflare R2 guide](https://v2.s3.dimah.dev/docs/providers/cloudflare-r2) for full details.

</Accordion>

<Accordion title="How do I display or preview the uploaded image?">

Use `useObjectUrl` to generate a temporary display URL, or construct your CDN URL if the bucket is public:

```tsx title="components/avatar-preview.tsx"
"use client";

import { useObjectUrl } from "@dimah-s3/react";

export function AvatarPreview({ avatarKey }: { avatarKey: string }) {
  const { url } = useObjectUrl({
    route: "avatar",
    objectKey: avatarKey,
    disposition: "inline", // [!code ++]
  });

  if (!url) return null;
  return (
    <img
      src={url}
      alt="User avatar"
      className="size-16 rounded-full object-cover"
    />
  );
}
```

</Accordion>

</Accordions>
