# Routes (https://v1.s3.dimah.dev/docs/server/routes)

A **route** is a named storage policy on your server. Each route owns its
constraints, object keys, and enabled operations (`upload`, `download`,
`delete`, and `multipart`).

The browser sends a route name with each request. The server enforces file
constraints, validates permissions, and generates object keys.

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

export const awsS3 = new S3Client({/* env */});

export const s3 = dimahS3({
  client: awsS3,
  bucket: process.env.S3_BUCKET!,
  routes: {
    avatar: route({
      upload: {
        fileTypes: ["image/*"],
        maxFileSize: 2 * 1024 * 1024,
      },
      download: true,
      delete: true,
    }),
    document: route({
      upload: {
        fileTypes: ["application/pdf", "image/*"],
        maxFileSize: 10 * 1024 * 1024,
      },
      download: true,
      delete: true,
    }),
    video: route({
      upload: {
        fileTypes: ["video/*"],
        maxFileSize: 500 * 1024 * 1024,
        multipart: true,
      },
    }),
  },
});
```

---

## Combining operations [#combining-operations]

Enable operations on the **same route** when they manage the same objects. For example, if users upload, view, and remove their avatars:

```ts
avatar: route({
  upload: {
    fileTypes: ["image/*"],
    maxFileSize: 2 * 1024 * 1024,
  },
  download: true, // [!code ++]
  delete: true, // [!code ++]
}),
```

With the default `keyPrefix` (the route name), follow-up operations (confirm,
download, and delete) must target keys within that route's namespace. That is
route isolation, not per-user ownership — see
[Security](https://v1.s3.dimah.dev/docs/server/security#route-namespace-vs-ownership).

---

## Key generation and object identity [#key-generation-and-object-identity]

By default, the server generates object keys formatted as:

```
{keyPrefix}/{uuid}/{sanitizedFileName}
```

`keyPrefix` defaults to the **route name** (`avatar` → `avatar/…`, `document`
→ `document/…`). The same prefix is used twice:

1. **Upload / multipart init** — the server _generates_ the key. The client
   sends a route name, not an object key.
2. **Confirm, download, delete, multipart follow-ups** — the client _sends_ a
   key. The server accepts it only if it sits under that route's prefix.

So `DELETE ?route=avatar&key=avatar/{uuid}/photo.jpg` is valid, and
`DELETE ?route=avatar&key=document/{uuid}/cv.pdf` returns `INVALID_KEY`.
Two routes cannot share or nest prefixes (`files` and `files/images`);
`dimahS3()` throws at startup.

<Callout type="warn" title="Disabling namespace isolation">
  `keyPrefix: false` allows follow-up operations to target any safe key in the
  bucket. Use it only when strict route or operation guards enforce the complete
  access policy.
</Callout>

To customize key generation or organize files by user/tenant, return `folder` or `key` from `upload.object`:

```ts
avatar: route({
  upload: {
    fileTypes: ["image/*"],
    maxFileSize: 2 * 1024 * 1024,
    object: async ({ request }) => {
      const session = await getSession(request);
      return {
        folder: `users/${session.userId}`, // [!code ++]
        // Results in: avatar/users/{userId}/{uuid}/{fileName}
      };
    },
  },
}),
```

---

## Type reference [#type-reference]

```ts
import type {
  DimahS3RouteConfig,
  UploadConfig,
  DownloadConfig,
  DeleteConfig,
  MultipartConfig,
} from "@dimah-s3/server";
```

### DimahS3RouteConfig [#dimahs3routeconfig]

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

---

### UploadConfig [#uploadconfig]

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

---

### DownloadConfig [#downloadconfig]

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

---

### DeleteConfig [#deleteconfig]

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

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

<Accordions>

<Accordion title="How do I overwrite the same avatar key instead of generating a UUID?">

Set `replace: "overwrite"` and return a deterministic `key` from `object`:

```ts
avatar: route({
  upload: {
    fileTypes: ["image/*"],
    maxFileSize: 2 * 1024 * 1024,
    replace: "overwrite", // [!code ++]
    object: async ({ request }) => {
      const session = await getSession(request);
      return {
        key: `avatars/${session.userId}/avatar.png`, // [!code ++]
      };
    },
  },
}),
```

</Accordion>

<Accordion title="How do I clean up the user's previous avatar after a new upload?">

Return `previousKey` from `upload.object`. After the new upload is confirmed,
dimah-s3 deletes the previous object from S3:

```ts
avatar: route({
  upload: {
    fileTypes: ["image/*"],
    maxFileSize: 2 * 1024 * 1024,
    object: async ({ request }) => {
      const user = await getCurrentUser(request);
      return {
        folder: `users/${user.id}`,
        previousKey: user.currentAvatarKey, // [!code ++]
      };
    },
  },
}),
```

</Accordion>

<Accordion title="How do I set storage class, Cache-Control, or object tags?">

Return them from `upload.object`. They are signed into PUT, POST, and
multipart init (and applied by `s3.put`) so the browser cannot change
them. Tag values are visible to the client — do not put secrets there.

```ts
avatar: route({
  upload: {
    fileTypes: ["image/*"],
    object: () => ({
      storageClass: "STANDARD_IA", // [!code ++]
      cacheControl: "max-age=31536000, immutable", // [!code ++]
      tagging: { purpose: "avatar" }, // [!code ++]
    }),
  },
}),
```

</Accordion>

<Accordion title="How do I enforce SHA-256 integrity checksums?">

For single-part POST or PUT uploads, set `checksum: true`. The React client
computes SHA-256 in the browser and the server includes it in the signed
request:

```ts
avatar: route({
  upload: {
    fileTypes: ["image/*"],
    maxFileSize: 2 * 1024 * 1024,
    checksum: true, // [!code ++]
  },
}),
```

Whole-file checksum enforcement is not currently applied to multipart part
signing. Confirmation and content inspection remain separate trust checks.

</Accordion>

<Accordion title="How do I stream downloads through my server without direct bucket URLs?">

Set `mode: "proxy"` on `download`. The returned URL points to the same-origin
`/api/s3/file` endpoint, which streams the object through your server:

```ts
avatar: route({
  download: {
    mode: "proxy", // [!code ++]
  },
}),
```

</Accordion>

</Accordions>
