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

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

Scaffold a full-stack project with preconfigured S3 routes and UI:

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

---

## Manual installation [#manual-installation]

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

```npm
npm i @dimah-s3/server @dimah-s3/react @aws-sdk/client-s3
```

### 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"
```

```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!,
  },
});

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

<Callout title="Cloudflare R2">
  Set `upload.method` to `"PUT"`. See [Cloudflare
  R2](https://v1.s3.dimah.dev/docs/providers/cloudflare-r2).
</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://v1.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://v1.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 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://v1.s3.dimah.dev/r/{name}.json" // [!code ++]
  }
}
```

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

```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 { 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 [#frequently-asked-questions]

<Accordions>

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

Add a `guard` on the route or instance. Throw `errors.unauthorized()` or `errors.forbidden()` if the session is invalid:

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

export const s3 = dimahS3({
  // ...
  routes: {
    avatar: route({
      guard: async ({ request }) => {
        // [!code ++]
        const session = await getSession(request); // [!code ++]
        if (!session) throw errors.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`:

```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` can place the confirmed 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: (results) => {
    setValue("avatarKey", results[0]?.key); // [!code ++]
  }, // [!code ++]
});
```

</Accordion>

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

Cloudflare R2 requires `upload: { method: "PUT" }` because it does not support presigned POST:

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

See the [Cloudflare R2 guide](https://v1.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>
