@xenterprises/fastify-xstorage
v1.3.0
Published
Fastify plugin for S3-compatible storage with simple, intuitive API. Supports AWS S3, Cloudflare R2, Digital Ocean Spaces and any S3-compatible provider.
Readme
@xenterprises/fastify-xstorage
Fastify v5 plugin for S3-compatible object storage with a simple, intuitive API. Upload, download, delete, list, copy, and move files on AWS S3, Cloudflare R2, Digital Ocean Spaces, or any S3-compatible provider. Built for Fastify apps that need private-by-default file storage with signed URLs, pre-signed direct uploads, and batch operations.
Install
npm install @xenterprises/fastify-xstorage fastify@5For HTTP file upload routes, also install @fastify/multipart@9 (not required by the plugin itself).
Minimal example
import Fastify from "fastify";
import xStorage from "@xenterprises/fastify-xstorage";
const fastify = Fastify({ logger: true });
await fastify.register(xStorage, {
endpoint: "https://nyc3.digitaloceanspaces.com",
region: "nyc3",
accessKeyId: process.env.STORAGE_ACCESS_KEY_ID, // the consumer owns env access
secretAccessKey: process.env.STORAGE_SECRET_ACCESS_KEY,
bucket: "my-bucket",
publicUrl: "https://my-bucket.nyc3.digitaloceanspaces.com",
});
const result = await fastify.xStorage.upload(buffer, "report.pdf", { folder: "docs" });
// → { key: "docs/report-a1b2c3d4e5f6g7h8.pdf", url: "https://…", size: 123456, contentType: "application/pdf", bucket: "my-bucket" }
const signedUrl = await fastify.xStorage.getSignedUrl(result.key, 3600);The plugin never reads process.env — all configuration arrives via the options object. Read env vars in your app and pass them in, as shown above.
Options
| Option | Type | Default | Required | Description |
|--------|------|---------|----------|-------------|
| accessKeyId | string | — | Yes | S3 access key ID. |
| secretAccessKey | string | — | Yes | S3 secret access key. |
| bucket | string | — | Yes | Bucket name. |
| publicUrl | string | — | Yes | Public URL base for the bucket (trailing slash is stripped). |
| endpoint | string | undefined | No* | S3 endpoint URL. Required for non-AWS providers (DO Spaces, R2). Omit for AWS S3. |
| region | string | "us-east-1" | No | AWS region or provider region ("auto" for R2). |
| forcePathStyle | boolean | true | No | Use path-style URLs (required for DO Spaces and R2; set false for AWS S3). |
| acl | string | "private" | No | Default ACL for uploads ("private" or "public-read"). |
Provider configuration
AWS S3
await fastify.register(xStorage, {
region: "us-east-1",
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
bucket: "my-bucket",
publicUrl: "https://my-bucket.s3.us-east-1.amazonaws.com",
forcePathStyle: false,
});Cloudflare R2
await fastify.register(xStorage, {
endpoint: "https://ACCOUNT_ID.r2.cloudflarestorage.com",
region: "auto",
accessKeyId: process.env.R2_ACCESS_KEY_ID,
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY,
bucket: "my-bucket",
publicUrl: "https://cdn.example.com",
});Decorators
All methods live on the single fastify.xStorage namespace. No request decorators are added.
| Method | Signature | Returns | Description |
|--------|-----------|---------|-------------|
| upload | (file: Buffer\|Stream, filename: string, options?) | Promise<UploadResult> | Upload a single file. |
| uploadMultiple | (files: Array<{file, filename, acl?}>, options?) | Promise<UploadResult[]> | Upload multiple files concurrently. |
| download | (key: string) | Promise<Buffer> | Download a file as a buffer. |
| delete | (key: string) | Promise<boolean> | Delete a file. |
| deleteMultiple | (keys: string[]) | Promise<{deleted, errors}> | Batch delete via a single S3 DeleteObjects call. |
| copy | (sourceKey, destinationKey, options?) | Promise<{key, url}> | Copy a file within the bucket. |
| move | (sourceKey, destinationKey, options?) | Promise<{key, url}> | Copy + delete source. |
| exists | (key: string) | Promise<boolean> | Check whether a file exists. |
| getMetadata | (key: string) | Promise<FileMetadata> | Content type, length, last modified, etag, custom metadata. |
| list | (prefix?, maxKeys = 1000) | Promise<FileInfo[]> | List files (single page). |
| listAll | (prefix?) | Promise<FileInfo[]> | List all files with automatic pagination. |
| getSignedUrl | (key, expiresIn = 3600) | Promise<string> | Pre-signed download URL. |
| getSignedUploadUrl | (key, options?) | Promise<{uploadUrl, key, publicUrl}> | Pre-signed URL for direct client-to-S3 uploads. |
| getPublicUrl | (key: string) | string | Public URL (synchronous; only reachable when the object ACL allows it). |
upload options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| folder | string | "" | Folder path prefix (e.g. "docs/2024"). |
| key | string | null | Exact storage key (overrides folder + filename generation). |
| contentType | string | auto-detected | MIME type (via mime-types, falls back to application/octet-stream). |
| metadata | object | {} | Custom S3 object metadata. |
| useRandomName | boolean | true | Append a random hex suffix to the filename. |
| acl | string | plugin acl | Per-file ACL override. |
getSignedUploadUrl options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| contentType | string | "application/octet-stream" | Expected MIME type of the upload. |
| expiresIn | number | 3600 | URL lifetime in seconds. |
| metadata | object | {} | Custom S3 object metadata. |
Helper utilities
Pure functions, no S3 access. Import via import { helpers } from "@xenterprises/fastify-xstorage" or @xenterprises/fastify-xstorage/helpers:
formatFileSize, generateUniqueFilename, isValidFileType, isImage, isPdf, isVideo, sanitizeFilename, getFileExtension, getFilenameWithoutExtension, buildStorageKey, getAspectRatio, calculateFitDimensions, getKeyFromUrl, groupFilesByFolder, isValidDimensions, generateResponsiveSizes.
Routes
None. The plugin only decorates the Fastify instance; routing (e.g. multipart upload endpoints) is up to the consumer.
Error behavior
Registration fails fast with descriptive errors that name the plugin, the option, and an example:
xstorage: missing required option `bucket` (string), e.g. `app.register(xStorage, { bucket: 'my-bucket' })`
xstorage: option `forcePathStyle` must be a boolean, e.g. `app.register(xStorage, { forcePathStyle: true })`Thrown for: missing/non-string accessKeyId, secretAccessKey, bucket, publicUrl; non-string endpoint, region, acl; non-boolean forcePathStyle.
Method calls validate inputs before touching S3 and throw Errors prefixed with [xStorage]:
| Error | When |
|-------|------|
| [xStorage] file is required for upload | null/undefined file passed to upload. |
| [xStorage] filename is required and must be a string | Missing/invalid filename in upload. |
| [xStorage] key is required and must be a string | Missing key in download/delete/exists/getMetadata/URL methods. |
| [xStorage] keys must be a non-empty array of strings | Invalid input to deleteMultiple. |
| [xStorage] files must be a non-empty array | Invalid input to uploadMultiple. |
| [xStorage] sourceKey is required and must be a string | Missing source key in copy/move. |
| [xStorage] destinationKey is required and must be a string | Missing destination key in copy/move. |
| [xStorage] expiresIn must be a positive number (seconds) | Invalid expiration for getSignedUrl. |
| [xStorage] Failed to <operation> …: <s3 message> | S3/presigner errors are wrapped with the operation and affected key. |
exists() is the only method that swallows an S3 error — a 404/NotFound returns false; any other error is rethrown.
Requirements
- Node.js >= 20
- Fastify ^5.0.0 (peer dependency)
License
Proprietary — All Rights Reserved X Enterprises. See LICENSE.
