Server Setup
Install `@dimah-s3/server`, configure S3, and mount your route handler.
Install
npm i @dimah-s3/server @aws-sdk/client-s3Configure 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.
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,
}),
},
});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.
Mount route handler
Adapters are available for all major JavaScript runtimes:
import { toNextJsHandler } from "@dimah-s3/server/next";
import { s3 } from "@/lib/s3";
export const { GET, POST, PUT, PATCH, DELETE } = toNextJsHandler(s3);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.
Server-side API (s3.api)
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:
"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(),
});
}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:
await s3.put({
route: "avatar",
fileName: "profile.png",
contentType: "image/png",
body: imageBuffer,
headers: await headers(),
});Type reference
import type { DimahS3Config } from "@dimah-s3/server";DimahS3Config
Prop
Type
Frequently asked questions
All handlers throw typed APIError objects with stable string codes and numeric HTTP status codes:
- A plain
Errorthrown inguard,*Guard, orobjectmaps to 403 Forbidden. - A plain
Errorthrown inonPresigned,onConfirmed, or otheron*hooks maps to 500 Internal Error. - Use the
errorshelper (e.g.throw errors.unauthorized()) to specify exact codes.
See Errors for the full list of error codes.
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()
adds per-user scope, not the prefix bound.
Setting keyPrefix: false disables that namespace boundary and requires strict
application guards. See
Security.
To customize the folder or key structure, use upload.object in your route definition.
Set basePath when initializing dimahS3:
export const s3 = dimahS3({
basePath: "/api/storage",
// ...
});Make sure the client specifies the same basePath (or full baseURL if hosted on a separate domain):
export const s3Client = createS3Client({
baseURL: "https://api.example.com/api/storage",
});