Routes
Named file routes — server-owned keys, constraints, and object policies.
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.
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
Enable operations on the same route when they manage the same objects. For example, if users upload, view, and remove their avatars:
avatar: route({
upload: {
fileTypes: ["image/*"],
maxFileSize: 2 * 1024 * 1024,
},
download: true,
delete: true,
}),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.
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:
- Upload / multipart init — the server generates the key. The client sends a route name, not an object key.
- 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.
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.
To customize key generation or organize files by user/tenant, return folder or key from upload.object:
avatar: route({
upload: {
fileTypes: ["image/*"],
maxFileSize: 2 * 1024 * 1024,
object: async ({ request }) => {
const session = await getSession(request);
return {
folder: `users/${session.userId}`,
// Results in: avatar/users/{userId}/{uuid}/{fileName}
};
},
},
}),Type reference
import type {
DimahS3RouteConfig,
UploadConfig,
DownloadConfig,
DeleteConfig,
MultipartConfig,
} from "@dimah-s3/server";DimahS3RouteConfig
Prop
Type
UploadConfig
Prop
Type
DownloadConfig
Prop
Type
DeleteConfig
Prop
Type
Frequently asked questions
Set replace: "overwrite" and return a deterministic key from object:
avatar: route({
upload: {
fileTypes: ["image/*"],
maxFileSize: 2 * 1024 * 1024,
replace: "overwrite",
object: async ({ request }) => {
const session = await getSession(request);
return {
key: `avatars/${session.userId}/avatar.png`,
};
},
},
}),Return previousKey from upload.object. After the new upload is confirmed,
dimah-s3 deletes the previous object from S3:
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,
};
},
},
}),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.
avatar: route({
upload: {
fileTypes: ["image/*"],
object: () => ({
storageClass: "STANDARD_IA",
cacheControl: "max-age=31536000, immutable",
tagging: { purpose: "avatar" },
}),
},
}),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:
avatar: route({
upload: {
fileTypes: ["image/*"],
maxFileSize: 2 * 1024 * 1024,
checksum: true,
},
}),Whole-file checksum enforcement is not currently applied to multipart part signing. Confirmation and content inspection remain separate trust checks.
Set mode: "proxy" on download. The returned URL points to the same-origin
/api/s3/file endpoint, which streams the object through your server:
avatar: route({
download: {
mode: "proxy",
},
}),