Usage
Construct a Files instance with an adapter, then call the same nine methods on it - swap the adapter to switch backends.
How it works
The shape is class + adapter injection. You construct a Files instance with an adapter, then call methods on the instance.
import { Files } from "files-sdk";
import { s3 } from "files-sdk/s3";
const files = new Files({
adapter: s3({ bucket: "uploads", region: "us-east-1" }),
});
await files.upload("avatars/abc.png", file, { contentType: "image/png" });
const stored = await files.download("avatars/abc.png");
const url = await files.url("avatars/abc.png", { expiresIn: 60 });Swap the adapter to switch backends - everything below the constructor stays the same:
import { r2 } from "files-sdk/r2";
import { vercelBlob } from "files-sdk/vercel-blob";
const files = new Files({ adapter: r2({ accountId, bucket: "uploads" }) });
// or
const files = new Files({
adapter: vercelBlob({ token: process.env.BLOB_READ_WRITE_TOKEN! }),
});Reads return a StoredFile - a File-shaped value with key, etag, and metadata added on top. Body accessors (arrayBuffer, text, stream, blob) are lazy on results from head and list, so listing a thousand objects doesn't fetch their bodies.
Quick start
import { Files, FilesError } from "files-sdk";
import { s3 } from "files-sdk/s3";
const files = new Files({
adapter: s3({ bucket: "uploads", region: "us-east-1" }),
});
// Upload
await files.upload("reports/q1.pdf", file, {
contentType: "application/pdf",
cacheControl: "public, max-age=31536000",
metadata: { userId: "123" },
});
// List with cursor pagination
const { items, cursor } = await files.list({ prefix: "reports/", limit: 50 });
// Sign a short-lived read URL
const url = await files.url("reports/q1.pdf", { expiresIn: 300 });
// Hand back a browser-direct upload contract
const upload = await files.signedUploadUrl("reports/q2.pdf", {
expiresIn: 600,
contentType: "application/pdf",
maxSize: 25_000_000,
});
// Handle normalized errors
try {
await files.download("missing.pdf");
} catch (err) {
if (err instanceof FilesError && err.code === "NotFound") return null;
throw err;
}File handles
When the same key comes up again and again, bind it once with files.file(key) and drop the repeated argument. A FileHandle is a thin wrapper over the same adapter methods - same behavior, just scoped to one key.
const avatar = files.file("avatars/abc.png");
await avatar.upload(file, { contentType: "image/png" });
if (await avatar.exists()) {
const meta = await avatar.head();
const url = await avatar.url({ expiresIn: 300 });
}
await avatar.delete();Working in bulk
upload, download, head, and exists each take a single key or an array, and delete takes one key or many. The array form fans out with bounded concurrency and returns a structured result that keeps successes and failures separate, in input order - so one bad key never sinks the whole batch.
// Upload several objects in one call
const { uploaded, errors } = await files.upload([
{ key: "a.txt", body: "alpha" },
{ key: "b.txt", body: "beta", contentType: "text/plain" },
]);See Bulk actions for the per-method result shapes, partial-failure handling, and how concurrency and stopOnError tune the fan-out.
Configuring the client
The constructor takes more than an adapter. Set a prefix to namespace every key, default timeout and retries for every call, or hooks to observe operations as they run - all optional, all overridable per call.
const files = new Files({
adapter: s3({ bucket: "uploads" }),
prefix: "users", // every key resolves under users/
timeout: 10_000, // default per-attempt timeout
retries: 3, // retry provider failures
});See Prefixes and the per-operation options (Timeouts, Retries, Cancellation, Hooks) for the full behavior.
Hooks
Pass hooks to the constructor to observe operations as they run - one place to wire logging, metrics, error reporting, and retry telemetry without wrapping every call. Each hook is fire-and-forget, like the onProgress upload callback: the SDK calls it but never awaits it, and a hook that throws can't fail the operation it observes. Payloads are caller-facing - the public key / keys you passed, never the internal prefixed path.
const files = new Files({
adapter: s3({ bucket: "uploads" }),
hooks: {
onAction(event) {
logger.info("files", event.type, event.status, event.key ?? event.keys);
},
onError(event) {
reportError(event.error, { action: event.type, key: event.key });
},
onRetry(event) {
metrics.increment("files.retry", { action: event.type });
},
},
});Three hooks live on the constructor: onAction runs when a call settles, onError when it rejects, and onRetry before each scheduled retry. A fourth callback, onProgress, is a per-call upload option rather than a constructor hook, but follows the same fire-and-forget contract.
Prop
Type
For the complete method surface and options, see the API reference.
Installation
Install files-sdk and the optional peer dependencies for the adapter you're using - nothing else is bundled.
Providers
A zero-dependency catalog of every provider the SDK ships and the environment variables each one reads - useful for building config UIs, sync engines, and onboarding flows.