# Server Setup (https://v1.s3.dimah.dev/docs/server/setup)

## Install [#install]

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

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

Initialize `dimahS3` by passing your `@aws-sdk/client-s3` instance and defining named routes.

Operations (`upload`, `download`, `delete`, `multipart`) are disabled by default. Enable only what each route needs.

```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,
      delete: true,
    }),
  },
});
```

<Callout title="Uploading more than 5 GiB">
  A single presigned POST cannot exceed **5 GiB**. For larger files, enable
  `upload.multipart` or set `upload.method` to `"PUT"` when your storage
  provider supports a single PUT of that size. The route's `maxFileSize` still
  applies.
</Callout>

---

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

Adapters are available for all major JavaScript runtimes:

<Tabs items={["Next.js", "Express", "Hono", "SvelteKit", "Fastify", "Elysia", "Node", "Web"]}>

<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 presign, download, and delete run on each request. Leave the
  handler uncached. A `use cache` wrapper would reuse one presign across
  requests.
</Callout>

</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();
// The adapter reads the raw stream, so mount it before JSON middleware.
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>

<Tab value="Fastify">

```ts title="server.ts"
import Fastify from "fastify";
import { toFastifyHandler } from "@dimah-s3/server/fastify";
import { s3 } from "./s3";

const app = Fastify();
app.all("/api/s3/*", toFastifyHandler(s3));
```

</Tab>

<Tab value="Elysia">

```ts title="src/index.ts"
import { Elysia } from "elysia";
import { toElysiaHandler } from "@dimah-s3/server/elysia";
import { s3 } from "./s3";

new Elysia().all("/api/s3/*", toElysiaHandler(s3)).listen(3000);
```

</Tab>

<Tab value="Node">

```ts title="server.ts"
import { createServer } from "node:http";
import { toNodeHandler } from "@dimah-s3/server/node";
import { s3 } from "./s3";

const handler = toNodeHandler(s3);

createServer((req, res) => {
  if (req.url?.startsWith("/api/s3")) return handler(req, res);
  res.statusCode = 404;
  res.end("Not found");
}).listen(3000);
```

</Tab>

<Tab value="Web">

```ts
export default {
  fetch(request: Request) {
    const url = new URL(request.url);
    if (url.pathname.startsWith("/api/s3")) {
      return s3.handler(request);
    }
    return new Response("Not found", { status: 404 });
  },
};
```

</Tab>

</Tabs>

---

## Server-side API (`s3.api`) [#server-side-api-s3api]

You can invoke S3 actions directly on the server without going through HTTP endpoints. Pass request `headers` so that route guards and auth hooks execute properly:

```ts title="app/actions.ts"
"use server";

import { headers } from "next/headers";
import { s3 } from "@/lib/s3";

export async function getAvatarDownloadUrl(key: string) {
  return s3.api.download({
    query: { route: "avatar", key },
    headers: await headers(), // [!code ++]
  });
}
```

Guards and `resolveScope` see that call as a `Request`: `headers` are copied, `query` is on the URL, and a JSON `body` stays readable with `request.clone().json()`. Pass `request` only when you already have the incoming one — that object is used as-is.

To upload files directly from the server (e.g. background jobs, image processing) while still executing route validation and `onConfirmed` hooks:

```ts
await s3.put({
  route: "avatar",
  fileName: "profile.png",
  contentType: "image/png",
  body: imageBuffer,
  headers: await headers(), // [!code ++]
});
```

---

## Type reference [#type-reference]

```ts
import type { DimahS3Config } from "@dimah-s3/server";
```

### DimahS3Config [#dimahs3config]

<AutoTypeTable
  path="packages/server/src/types/config.ts"
  name="DimahS3Config"
/>

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

<Accordions>

<Accordion title="How do error handling and status codes work?">

All handlers throw typed `APIError` objects with stable string codes and numeric HTTP status codes:

- A plain `Error` thrown in `guard`, `*Guard`, or `object` maps to **403 Forbidden**.
- A plain `Error` thrown in `onPresigned`, `onConfirmed`, or other `on*` hooks maps to **500 Internal Error**.
- Use the `errors` helper (e.g. `throw errors.unauthorized()`) to specify exact codes.

See [Errors](https://v1.s3.dimah.dev/docs/server/errors) for the full list of error codes.

</Accordion>

<Accordion title="How does key generation and prefix isolation work?">

By default, dimah-s3 isolates object keys by route prefix (the route name):

```
avatar/{uuid}/{fileName}
```

Upload generates that key. Confirm, download, and delete accept a client-sent
key only if it stays under the same prefix; otherwise they return
`INVALID_KEY`. That check is always in `@dimah-s3/server` — [`db()`](https://v1.s3.dimah.dev/docs/db)
adds per-user scope, not the prefix bound.

Setting `keyPrefix: false` disables that namespace boundary and requires strict
application guards. See
[Security](https://v1.s3.dimah.dev/docs/server/security#route-namespace-vs-ownership).

To customize the folder or key structure, use `upload.object` in your route definition.

</Accordion>

<Accordion title="How do I change the base path or support cross-origin APIs?">

Set `basePath` when initializing `dimahS3`:

```ts
export const s3 = dimahS3({
  basePath: "/api/storage", // [!code ++]
  // ...
});
```

Make sure the client specifies the same `basePath` (or full `baseURL` if hosted on a separate domain):

```ts
export const s3Client = createS3Client({
  baseURL: "https://api.example.com/api/storage", // [!code ++]
});
```

</Accordion>

</Accordions>
