@xenterprises/fastify-xstorage
v1.2.1
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 file 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. Default ACL is private — use signed URLs for secure access.
Install
npm install @xenterprises/fastify-xstorageFor HTTP file uploads, also install:
npm install @fastify/multipart@9Quick Start
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: "us-east-1",
accessKeyId: process.env.STORAGE_ACCESS_KEY_ID,
secretAccessKey: process.env.STORAGE_SECRET_ACCESS_KEY,
bucket: "my-bucket",
publicUrl: "https://my-bucket.nyc3.digitaloceanspaces.com",
});
// Upload a file
const result = await fastify.xStorage.upload(buffer, "report.pdf", { folder: "docs" });
// → { key: "docs/report-a1b2c3d4.pdf", url: "https://…/docs/report-a1b2c3d4.pdf", size: 123456, contentType: "application/pdf", bucket: "my-bucket" }
// Get a signed download URL (1 hour)
const url = await fastify.xStorage.getSignedUrl(result.key, 3600);
await fastify.listen({ port: 3000 });Plugin Options
| Option | Type | Default | Required | Description |
|--------|------|---------|----------|-------------|
| endpoint | string | — | No* | S3 endpoint URL. Required for non-AWS providers. |
| region | string | "us-east-1" | No | AWS region or provider region. |
| 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. |
| forcePathStyle | boolean | true | No | Use path-style URLs (required for DO Spaces, R2). |
| acl | string | "private" | No | Default ACL for uploads ("private" or "public-read"). |
*For AWS S3, omit endpoint and set region instead.
Provider Configuration
Digital Ocean Spaces
await fastify.register(xStorage, {
endpoint: "https://nyc3.digitaloceanspaces.com",
region: "nyc3",
accessKeyId: process.env.DO_SPACES_KEY,
secretAccessKey: process.env.DO_SPACES_SECRET,
bucket: "my-bucket",
publicUrl: "https://my-bucket.nyc3.digitaloceanspaces.com",
forcePathStyle: true,
});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",
forcePathStyle: true,
});Decorated Properties
All methods are available on fastify.xStorage.
| Method | Signature | Returns | Description |
|--------|-----------|---------|-------------|
| upload | (file, filename, options?) | Promise<UploadResult> | Upload a single file. |
| uploadMultiple | (files, options?) | Promise<UploadResult[]> | Upload multiple files concurrently. |
| download | (key) | Promise<Buffer> | Download a file as a buffer. |
| delete | (key) | Promise<boolean> | Delete a file. |
| deleteMultiple | (keys) | Promise<{deleted, errors}> | Batch-delete using S3 DeleteObjects. |
| copy | (sourceKey, destKey, options?) | Promise<{key, url}> | Copy a file. |
| move | (sourceKey, destKey, options?) | Promise<{key, url}> | Move a file (copy + delete source). |
| exists | (key) | Promise<boolean> | Check if a file exists. |
| getMetadata | (key) | Promise<Metadata> | Get file metadata (size, type, etag, etc.). |
| list | (prefix?, maxKeys?) | Promise<FileInfo[]> | List files (single page, default 1000). |
| listAll | (prefix?) | Promise<FileInfo[]> | List all files with automatic pagination. |
| getSignedUrl | (key, expiresIn?) | Promise<string> | Pre-signed download URL (default: 1 hour). |
| getSignedUploadUrl | (key, options?) | Promise<{uploadUrl, key, publicUrl}> | Pre-signed upload URL for direct client uploads. |
| getPublicUrl | (key) | string | Public URL (only works with public-read ACL). |
Upload Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| folder | string | "" | Folder path prefix (e.g. "docs/2024"). |
| key | string | null | Custom storage key (overrides folder + filename). |
| contentType | string | auto-detected | MIME type. |
| metadata | object | {} | Custom S3 metadata. |
| useRandomName | boolean | true | Append random hex to filename. |
| acl | string | plugin default | Per-file ACL override. |
getSignedUploadUrl Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| contentType | string | "application/octet-stream" | Expected MIME type. |
| expiresIn | number | 3600 | URL lifetime in seconds. |
| metadata | object | {} | Custom S3 metadata. |
Environment Variables
| Variable | Required | Description |
|----------|----------|-------------|
| STORAGE_ENDPOINT | No* | S3 endpoint URL |
| STORAGE_REGION | No | AWS region (default: us-east-1) |
| STORAGE_ACCESS_KEY_ID | Yes | S3 access key |
| STORAGE_SECRET_ACCESS_KEY | Yes | S3 secret key |
| STORAGE_BUCKET | Yes | Bucket name |
| STORAGE_PUBLIC_URL | Yes | Public URL base |
*Required for non-AWS providers (DO Spaces, R2, etc.).
Error Reference
All errors thrown by the plugin are prefixed with [xStorage].
| Error | When |
|-------|------|
| [xStorage] accessKeyId is required and must be a string | Missing or invalid accessKeyId at startup. |
| [xStorage] secretAccessKey is required and must be a string | Missing or invalid secretAccessKey at startup. |
| [xStorage] bucket is required and must be a string | Missing or invalid bucket at startup. |
| [xStorage] publicUrl is required and must be a string | Missing or invalid publicUrl at startup. |
| [xStorage] endpoint must be a string when provided | Non-string endpoint at startup. |
| [xStorage] forcePathStyle must be a boolean | Non-boolean forcePathStyle at startup. |
| [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/metadata/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 upload file "…": … | S3 upload error. |
| [xStorage] Failed to download file "…": … | S3 download error. |
| [xStorage] Failed to delete file "…": … | S3 delete error. |
| [xStorage] Failed to delete multiple files: … | S3 batch delete error. |
| [xStorage] Failed to copy "…" to "…": … | S3 copy error. |
| [xStorage] Failed to check existence of "…": … | S3 head error (not a 404). |
| [xStorage] Failed to get metadata for "…": … | S3 head error. |
| [xStorage] Failed to list files with prefix "…": … | S3 list error. |
| [xStorage] Failed to generate signed URL for "…": … | Presigner error. |
| [xStorage] Failed to generate signed upload URL for "…": … | Presigner error. |
Helper Utilities
Exported separately: import { helpers } from "@xenterprises/fastify-xstorage" or import { helpers } from "@xenterprises/fastify-xstorage/helpers".
| Helper | Signature | Returns | Description |
|--------|-----------|---------|-------------|
| formatFileSize | (bytes) | string | Human-readable file size (e.g. "1.18 MB"). |
| isImage | (filename) | boolean | Check image extension (jpg, png, webp, avif, svg, gif, bmp). |
| isPdf | (filename) | boolean | Check PDF extension. |
| isVideo | (filename) | boolean | Check video extension (mp4, mov, webm, etc.). |
| isValidFileType | (filename, allowedTypes) | boolean | Check against allowed extensions list. |
| sanitizeFilename | (filename) | string | Remove unsafe characters, lowercase. |
| getFileExtension | (filename) | string | Extract lowercase extension. |
| getFilenameWithoutExtension | (filename) | string | Filename without extension. |
| generateUniqueFilename | (filename) | string | Add timestamp to filename. |
| buildStorageKey | (filename, options) | string | Build key with folder/prefix/suffix/timestamp. |
| getAspectRatio | (width, height) | string | Calculate aspect ratio (e.g. "16:9"). |
| calculateFitDimensions | (w, h, maxW, maxH) | {width, height} | Fit within bounds preserving ratio. |
| generateResponsiveSizes | (width, height) | Array | Responsive breakpoint sizes. |
| isValidDimensions | (w, h, constraints) | boolean | Validate against min/max dimensions. |
| getKeyFromUrl | (url, publicUrl) | string | Extract storage key from full URL. |
| groupFilesByFolder | (files) | object | Group file objects by folder path. |
How It Works
Registration — The plugin validates all options at startup and creates an
S3Clientfrom@aws-sdk/client-s3. It stores the client, bucket name, public URL, and default ACL in a closure.Decoration — A
storageMethodsobject is created with closures over the S3 client and config, then attached to the Fastify instance asfastify.xStorage.Uploads —
upload()generates a storage key (with optional random suffix), detects MIME type viamime-types, and sends aPutObjectCommand. The returned object includes the key, public URL, size, and content type for database storage.Signed URLs —
getSignedUrl()uses@aws-sdk/s3-request-presignerto create time-limited download URLs for private files.getSignedUploadUrl()does the same for direct client-to-S3 uploads, bypassing the server for large files.Batch operations —
uploadMultiple()delegates toupload()withPromise.all.deleteMultiple()usesDeleteObjectsCommandfor efficient batch deletion in a single S3 API call.Error handling — All methods validate inputs before touching S3 and wrap S3 errors with
[xStorage]prefix and the affected key for debuggability.
License
UNLICENSED
