https://github.com/th3hero/express-storage
express-storage is an easy-to-use Express middleware for handling file uploads across multiple storage providers like local, S3, GCS, and OCI buckets. It supports presigned and normal uploads with simple configuration through config files or environment variables.
https://github.com/th3hero/express-storage
aws-s3 azure-blob-storage cloud-storage express expressjs file-upload google-cloud-storage middleware multer multi-cloud nodejs presigned-url storage-abstraction storage-s3 typescript
Last synced: about 1 month ago
JSON representation
express-storage is an easy-to-use Express middleware for handling file uploads across multiple storage providers like local, S3, GCS, and OCI buckets. It supports presigned and normal uploads with simple configuration through config files or environment variables.
- Host: GitHub
- URL: https://github.com/th3hero/express-storage
- Owner: th3hero
- License: mit
- Created: 2025-07-30T15:59:58.000Z (8 months ago)
- Default Branch: main
- Last Pushed: 2026-02-04T20:39:44.000Z (about 2 months ago)
- Last Synced: 2026-02-05T08:48:25.025Z (about 2 months ago)
- Topics: aws-s3, azure-blob-storage, cloud-storage, express, expressjs, file-upload, google-cloud-storage, middleware, multer, multi-cloud, nodejs, presigned-url, storage-abstraction, storage-s3, typescript
- Language: TypeScript
- Homepage:
- Size: 661 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- Contributing: CONTRIBUTING.md
- License: LICENSE
- Code of conduct: CODE_OF_CONDUCT.md
Awesome Lists containing this project
README
# Express Storage
**Express.js file upload middleware for AWS S3, Google Cloud Storage, Azure Blob Storage, and local disk — one unified API, zero vendor lock-in.**
Express Storage is a TypeScript-first file upload library for Node.js and Express. Upload files to AWS S3, Google Cloud Storage (GCS), Azure Blob Storage, or local disk using a single API. Switch cloud providers by changing one environment variable — no code changes needed. Built-in presigned URL support, file validation, streaming uploads, and security protection make it a production-ready alternative to multer-s3 that works with every major cloud provider.
[](https://www.npmjs.com/package/express-storage)
[](https://www.npmjs.com/package/express-storage)
[](https://bundlephobia.com/package/express-storage)
[](https://www.typescriptlang.org/)
[](https://opensource.org/licenses/MIT)
[](https://nodejs.org)
[](https://github.com/th3hero/express-storage)
---
## Table of Contents
- [Features](#features)
- [Quick Start](#quick-start)
- [Supported Storage Providers](#supported-storage-providers)
- [Error Codes](#error-codes)
- [Security Features](#security-features)
- [Presigned URLs: Client-Side Uploads](#presigned-urls-client-side-uploads)
- [Large File Uploads](#large-file-uploads)
- [API Reference](#api-reference)
- [Environment Variables](#environment-variables)
- [Lifecycle Hooks](#lifecycle-hooks)
- [Type-Safe Results](#type-safe-results)
- [Configurable Concurrency](#configurable-concurrency)
- [Lifecycle Management](#lifecycle-management)
- [Custom Rate Limiting](#custom-rate-limiting)
- [Utilities](#utilities)
- [Real-World Examples](#real-world-examples)
- [Migrating Between Providers](#migrating-between-providers)
- [Migrating from v2 to v3](#migrating-from-v2-to-v3)
- [Why Express Storage over Alternatives?](#why-express-storage-over-alternatives)
- [TypeScript Support](#typescript-support)
- [Contributing](#contributing)
---
## Features
- **One API, Four Providers** — Write upload code once. Deploy to AWS S3, GCS, Azure, or local disk.
- **Presigned URLs** — Client-side uploads that bypass your server, with per-provider constraint enforcement.
- **File Validation** — Size limits, MIME type checks, and extension filtering before storage.
- **Security Built In** — Path traversal prevention, filename sanitization, null byte protection.
- **TypeScript Native** — Full type safety with discriminated unions. No `any` types.
- **Streaming Uploads** — Automatic multipart/streaming for files over 100MB.
- **Zero Config Switching** — Change `FILE_DRIVER=local` to `FILE_DRIVER=s3` and you're done.
- **Lifecycle Hooks** — Tap into upload/delete events for logging, virus scanning, or audit trails.
- **Batch Operations** — Upload or delete multiple files in parallel with concurrency control and `AbortSignal` support.
- **Custom Rate Limiting** — Built-in in-memory limiter or plug in your own (Redis, Memcached, etc.).
- **Lightweight** — Install only the cloud SDK you need. No dependency bloat.
---
## Quick Start
### Installation
```bash
npm install express-storage
```
Then install only the cloud SDK you need:
```bash
# For AWS S3
npm install @aws-sdk/client-s3 @aws-sdk/lib-storage @aws-sdk/s3-request-presigner
# For Google Cloud Storage
npm install @google-cloud/storage
# For Azure Blob Storage
npm install @azure/storage-blob @azure/identity
```
Local storage works out of the box with no additional dependencies.
### Basic Setup
```typescript
import express from "express";
import multer from "multer";
import { StorageManager } from "express-storage";
const app = express();
const upload = multer();
const storage = new StorageManager();
app.post("/upload", upload.single("file"), async (req, res) => {
const result = await storage.uploadFile(req.file, {
maxSize: 10 * 1024 * 1024, // 10MB limit
allowedMimeTypes: ["image/jpeg", "image/png", "application/pdf"],
});
if (result.success) {
res.json({ reference: result.reference, url: result.fileUrl });
} else {
res.status(400).json({ error: result.error });
}
});
```
### Environment Configuration
Create a `.env` file:
```env
# Choose your storage provider
FILE_DRIVER=local
# For local storage
LOCAL_PATH=uploads
# For AWS S3
FILE_DRIVER=s3
BUCKET_NAME=my-bucket
AWS_REGION=us-east-1
AWS_ACCESS_KEY=your-key
AWS_SECRET_KEY=your-secret
# For Google Cloud Storage
FILE_DRIVER=gcs
BUCKET_NAME=my-bucket
GCS_PROJECT_ID=my-project
# For Azure Blob Storage
FILE_DRIVER=azure
BUCKET_NAME=my-container
AZURE_CONNECTION_STRING=your-connection-string
```
That's it. Your upload code stays the same regardless of which provider you choose.
---
## Supported Storage Providers
| Provider | Direct Upload | Presigned URLs | Best For |
| ---------------- | ------------- | ----------------- | ------------------------- |
| **Local Disk** | `local` | — | Development, small apps |
| **AWS S3** | `s3` | `s3-presigned` | Most production apps |
| **Google Cloud** | `gcs` | `gcs-presigned` | GCP-hosted applications |
| **Azure Blob** | `azure` | `azure-presigned` | Azure-hosted applications |
---
## Error Codes
Every error result includes a `code` field for programmatic error handling — no more parsing error strings:
```typescript
const result = await storage.uploadFile(file, {
maxSize: 5 * 1024 * 1024,
allowedMimeTypes: ["image/jpeg", "image/png"],
});
if (!result.success) {
switch (result.code) {
case "FILE_TOO_LARGE":
res.status(413).json({ error: "File is too large" });
break;
case "INVALID_MIME_TYPE":
res.status(415).json({ error: "Unsupported file type" });
break;
case "RATE_LIMITED":
res.status(429).json({ error: "Too many requests" });
break;
default:
res.status(400).json({ error: result.error });
}
}
```
| Code | When |
| -------------------------- | -------------------------------------------------------------- |
| `NO_FILE` | No file provided to upload |
| `FILE_EMPTY` | File has zero bytes |
| `FILE_TOO_LARGE` | File exceeds `maxSize` or `maxFileSize` |
| `INVALID_MIME_TYPE` | MIME type not in `allowedMimeTypes` |
| `INVALID_EXTENSION` | Extension not in `allowedExtensions` |
| `INVALID_FILENAME` | Filename is empty, too long, or contains illegal characters |
| `INVALID_INPUT` | Bad argument (e.g., non-numeric fileSize, missing fileName) |
| `PATH_TRAVERSAL` | Path contains `..`, `\0`, or other traversal sequences |
| `FILE_NOT_FOUND` | File doesn't exist (delete, validate, view) |
| `VALIDATION_FAILED` | Post-upload validation failed (content type or size mismatch) |
| `RATE_LIMITED` | Presigned URL rate limit exceeded |
| `HOOK_ABORTED` | A `beforeUpload` or `beforeDelete` hook threw |
| `PRESIGNED_NOT_SUPPORTED` | Local driver doesn't support presigned URLs |
| `PROVIDER_ERROR` | Cloud provider SDK error (network, auth, permissions) |
---
## Security Features
File uploads are one of the most exploited attack vectors in web applications. Express Storage protects you by default.
### Path Traversal Prevention
Attackers try filenames like `../../../etc/passwd` to escape your upload directory. We block this:
```typescript
// These malicious filenames are automatically rejected
"../secret.txt"; // Blocked: path traversal
"..\\config.json"; // Blocked: Windows path traversal
"file\0.txt"; // Blocked: null byte injection
```
### Automatic Filename Sanitization
User-provided filenames can't be trusted. We transform them into safe, unique identifiers:
```
User uploads: "My Photo (1).jpg"
Stored as: "1706123456789_a1b2c3d4e5_my_photo_1_.jpg"
```
The format `{timestamp}_{random}_{sanitized_name}` prevents collisions and removes dangerous characters.
### File Validation
Validate before processing. Reject before storing.
```typescript
await storage.uploadFile(file, {
maxSize: 5 * 1024 * 1024, // 5MB limit
allowedMimeTypes: ["image/jpeg", "image/png"],
allowedExtensions: [".jpg", ".png"],
});
```
### Presigned URL Security
For S3 and GCS, file constraints are enforced at the URL level — clients physically cannot upload the wrong file type or size. For Azure (which doesn't support URL-level constraints), we validate after upload and automatically delete invalid files.
---
## Presigned URLs: Client-Side Uploads
Large files shouldn't flow through your server. Presigned URLs let clients upload directly to cloud storage.
### The Flow
```
1. Client → Your Server: "I want to upload photo.jpg (2MB, image/jpeg)"
2. Your Server → Client: "Here's a presigned URL, valid for 10 minutes"
3. Client → Cloud Storage: Uploads directly (your server never touches the bytes)
4. Client → Your Server: "Upload complete, please verify"
5. Your Server: Confirms file exists, returns permanent URL
```
### Implementation
```typescript
// Step 1: Generate upload URL
app.post("/upload/init", async (req, res) => {
const { fileName, contentType, fileSize } = req.body;
const result = await storage.generateUploadUrl(
fileName,
contentType,
fileSize,
"user-uploads", // Optional folder
);
res.json({
uploadUrl: result.uploadUrl,
reference: result.reference, // Save this for later
});
});
// Step 2: Confirm upload
app.post("/upload/confirm", async (req, res) => {
const { reference, expectedContentType, expectedFileSize } = req.body;
const result = await storage.validateAndConfirmUpload(reference, {
expectedContentType,
expectedFileSize,
});
if (result.success) {
res.json({ viewUrl: result.viewUrl });
} else {
res.status(400).json({ error: result.error });
}
});
```
### Provider-Specific Behavior
| Provider | Content-Type Enforced | File Size Enforced | Post-Upload Validation |
| -------- | --------------------- | ------------------ | ---------------------- |
| S3 | At URL level | At URL level | Optional |
| GCS | At URL level | At URL level | Optional |
| Azure | **Not enforced** | **Not enforced** | **Required** |
For Azure, always call `validateAndConfirmUpload()` with expected values. Invalid files are automatically deleted.
---
## Large File Uploads
For files larger than 100MB, we recommend using **presigned URLs** instead of direct server uploads. Here's why:
### Memory Efficiency
When you upload through your server, the entire file must be buffered in memory (or stored temporarily on disk). For a 500MB video file, that's 500MB of RAM per concurrent upload. With presigned URLs, the file goes directly to cloud storage — your server only handles small JSON requests.
### Automatic Streaming
For files that must go through your server, Express Storage automatically uses streaming uploads for files larger than 100MB:
- **S3**: Uses multipart upload with 10MB chunks
- **GCS**: Uses resumable uploads with streaming
- **Azure**: Uses block upload with streaming
This happens transparently — you don't need to change your code.
### Recommended Approach for Large Files
```typescript
// Frontend: Request presigned URL
const { uploadUrl, reference } = await fetch("/api/upload/init", {
method: "POST",
body: JSON.stringify({
fileName: "large-video.mp4",
contentType: "video/mp4",
fileSize: 524288000, // 500MB
}),
}).then((r) => r.json());
// Frontend: Upload directly to cloud (bypasses your server!)
await fetch(uploadUrl, {
method: "PUT",
body: file,
headers: { "Content-Type": "video/mp4" },
});
// Frontend: Confirm upload
await fetch("/api/upload/confirm", {
method: "POST",
body: JSON.stringify({ reference }),
});
```
### Size Limits
| Scenario | Recommended Limit | Reason |
| ------------------------------ | ----------------- | ------------------------------ |
| Direct upload (memory storage) | < 100MB | Node.js memory constraints |
| Direct upload (disk storage) | < 500MB | Temp file management |
| Presigned URL upload | 5GB+ | Limited only by cloud provider |
---
## API Reference
### StorageManager
The main class you'll interact with.
```typescript
import { StorageManager } from "express-storage";
// Use environment variables
const storage = new StorageManager();
// Or configure programmatically
const storage = new StorageManager({
driver: "s3",
credentials: {
bucketName: "my-bucket",
awsRegion: "us-east-1",
maxFileSize: 50 * 1024 * 1024, // 50MB
},
logger: console, // Optional: enable debug logging
});
```
### File Upload Methods
```typescript
// Single file
const result = await storage.uploadFile(file, validation?, options?);
// Multiple files (processed in parallel with concurrency limits)
const results = await storage.uploadFiles(files, validation?, options?);
```
### Presigned URL Methods
```typescript
// Generate upload URL with constraints
const result = await storage.generateUploadUrl(fileName, contentType?, fileSize?, folder?);
// Generate view URL for existing file
const result = await storage.generateViewUrl(reference);
// Validate upload (required for Azure, recommended for all)
const result = await storage.validateAndConfirmUpload(reference, options?);
// Batch operations
const results = await storage.generateUploadUrls(files, folder?);
const results = await storage.generateViewUrls(references);
```
### File Management
```typescript
// Delete single file (returns DeleteResult with error details on failure)
const result = await storage.deleteFile(reference);
if (!result.success) console.log(result.error, result.code);
// Delete multiple files
const results = await storage.deleteFiles(references);
// Get file metadata without downloading
const info = await storage.getMetadata(reference);
if (info) console.log(info.name, info.size, info.contentType, info.lastModified);
// List files with pagination
const result = await storage.listFiles(prefix?, maxResults?, continuationToken?);
```
### Upload Options
```typescript
interface UploadOptions {
contentType?: string; // Override detected type
metadata?: Record; // Custom metadata
cacheControl?: string; // e.g., 'max-age=31536000'
contentDisposition?: string; // e.g., 'attachment; filename="doc.pdf"'
}
// Example: Upload with caching headers
await storage.uploadFile(file, undefined, {
cacheControl: "public, max-age=31536000",
metadata: { uploadedBy: "user-123" },
});
```
### Validation Options
```typescript
interface FileValidationOptions {
maxSize?: number; // Maximum file size in bytes
allowedMimeTypes?: string[]; // e.g., ['image/jpeg', 'image/png']
allowedExtensions?: string[]; // e.g., ['.jpg', '.png']
}
```
---
## Environment Variables
### Core Settings
| Variable | Description | Default |
| ---------------------- | ----------------------------------- | ------------------------ |
| `FILE_DRIVER` | Storage driver to use | `local` |
| `BUCKET_NAME` | Cloud storage bucket/container name | — |
| `BUCKET_PATH` | Default folder path within bucket | `""` (root) |
| `LOCAL_PATH` | Directory for local storage | `public/express-storage` |
| `PRESIGNED_URL_EXPIRY` | URL validity in seconds | `600` (10 min) |
| `MAX_FILE_SIZE` | Maximum upload size in bytes | `5368709120` (5GB) |
### AWS S3
| Variable | Description |
| ---------------- | ----------------------------------------------- |
| `AWS_REGION` | AWS region (e.g., `us-east-1`) |
| `AWS_ACCESS_KEY` | Access key ID (optional if using IAM roles) |
| `AWS_SECRET_KEY` | Secret access key (optional if using IAM roles) |
### Google Cloud Storage
| Variable | Description |
| ----------------- | ------------------------------------------------ |
| `GCS_PROJECT_ID` | Google Cloud project ID |
| `GCS_CREDENTIALS` | Path to service account JSON (optional with ADC) |
### Azure Blob Storage
| Variable | Description |
| ------------------------- | ------------------------------------------------- |
| `AZURE_CONNECTION_STRING` | Full connection string (recommended) |
| `AZURE_ACCOUNT_NAME` | Storage account name (alternative) |
| `AZURE_ACCOUNT_KEY` | Storage account key (alternative) |
**Note**: Azure uses `BUCKET_NAME` for the container name (same as S3/GCS).
---
## Lifecycle Hooks
Hooks let you tap into the upload/delete lifecycle without modifying drivers. Perfect for logging, virus scanning, metrics, or audit trails.
```typescript
const storage = new StorageManager({
driver: "s3",
hooks: {
beforeUpload: async (file) => {
await virusScan(file.buffer); // Throw to abort upload
},
afterUpload: (result, file) => {
auditLog("file_uploaded", { result, originalName: file.originalname });
},
beforeDelete: async (reference) => {
await checkPermissions(reference);
},
afterDelete: (reference, success) => {
if (success) auditLog("file_deleted", { reference });
},
onError: (error, context) => {
metrics.increment("storage.error", { operation: context.operation });
},
},
});
```
All hooks are optional and async-safe. `beforeUpload` and `beforeDelete` can throw to abort the operation — the error message is included in the result.
---
## Type-Safe Results
All result types use TypeScript discriminated unions. Check `result.success` and TypeScript narrows the type automatically:
```typescript
const result = await storage.uploadFile(file);
if (result.success) {
console.log(result.reference); // stored file path (for delete/view/getMetadata)
console.log(result.fileUrl); // URL to access the file
} else {
console.log(result.error); // TypeScript knows this exists
}
```
This applies to all result types: `FileUploadResult`, `DeleteResult`, `PresignedUrlResult`, `BlobValidationResult`, and `ListFilesResult`.
---
## Configurable Concurrency
Control how many parallel operations run in batch methods:
```typescript
const storage = new StorageManager({
driver: "s3",
concurrency: 5, // Applies to uploadFiles, deleteFiles, generateUploadUrls, etc.
});
```
Default is 10. Lower it for rate-limited APIs or resource-constrained environments.
### Cancellable Batch Operations
All batch methods accept an `AbortSignal` for cancelling long-running operations mid-flight:
```typescript
const controller = new AbortController();
// Cancel after 5 seconds
setTimeout(() => controller.abort(), 5000);
try {
const results = await storage.uploadFiles(files, validation, options, {
signal: controller.signal,
});
} catch (error) {
console.log("Upload batch was cancelled");
}
// Also works with deleteFiles, generateUploadUrls, generateViewUrls
await storage.deleteFiles(references, { signal: controller.signal });
```
---
## Lifecycle Management
Clean up resources when you're done with a StorageManager instance:
```typescript
const storage = new StorageManager({ driver: "s3", rateLimiter: { maxRequests: 100 } });
// ... use storage ...
// Release resources (clears factory cache entry and rate limiter)
storage.destroy();
```
This is especially useful in tests, serverless functions, or any environment where StorageManager instances are created and discarded frequently.
---
## Custom Rate Limiting
The built-in rate limiter works for single-process apps. For clustered deployments, provide your own adapter:
```typescript
import { StorageManager, RateLimiterAdapter } from "express-storage";
// or: import { RateLimiterAdapter } from "express-storage"; // types are always at top level
// Built-in in-memory limiter
const storage = new StorageManager({
driver: "s3",
rateLimiter: { maxRequests: 100, windowMs: 60000 },
});
// Custom Redis-backed limiter
class RedisRateLimiter implements RateLimiterAdapter {
async tryAcquire() {
/* Redis INCR + EXPIRE */
}
async getRemainingRequests() {
/* ... */
}
async getResetTime() {
/* ... */
}
}
const storage = new StorageManager({
driver: "s3",
rateLimiter: new RedisRateLimiter(redisClient),
});
```
---
## Utilities
Express Storage includes battle-tested utilities you can use directly.
### Retry with Exponential Backoff
```typescript
import { withRetry } from "express-storage/utils";
const result = await withRetry(() => storage.uploadFile(file), {
maxAttempts: 3,
baseDelay: 1000,
maxDelay: 10000,
exponentialBackoff: true,
});
```
### File Type Helpers
```typescript
import {
isImageFile,
isDocumentFile,
getFileExtension,
formatFileSize,
} from "express-storage/utils";
isImageFile("image/jpeg"); // true
isDocumentFile("application/pdf"); // true
getFileExtension("photo.jpg"); // '.jpg'
formatFileSize(1048576); // '1 MB'
```
### Custom Logging
```typescript
import { StorageManager, type Logger } from "express-storage";
const logger: Logger = {
debug: (msg, ...args) => console.debug(`[Storage] ${msg}`, ...args),
info: (msg, ...args) => console.info(`[Storage] ${msg}`, ...args),
warn: (msg, ...args) => console.warn(`[Storage] ${msg}`, ...args),
error: (msg, ...args) => console.error(`[Storage] ${msg}`, ...args),
};
const storage = new StorageManager({ driver: "s3", logger });
```
---
## Real-World Examples
### Profile Picture Upload
```typescript
app.post("/users/:id/avatar", upload.single("avatar"), async (req, res) => {
const result = await storage.uploadFile(
req.file,
{
maxSize: 2 * 1024 * 1024, // 2MB
allowedMimeTypes: ["image/jpeg", "image/png", "image/webp"],
},
{
cacheControl: "public, max-age=86400",
metadata: { userId: req.params.id },
},
);
if (result.success) {
await db.users.update(req.params.id, { reference: result.reference, avatarUrl: result.fileUrl });
res.json({ avatarUrl: result.fileUrl });
} else {
res.status(400).json({ error: result.error });
}
});
```
### Document Upload with Presigned URLs
```typescript
// Frontend requests upload URL
app.post("/documents/request-upload", async (req, res) => {
const { fileName, fileSize } = req.body;
const result = await storage.generateUploadUrl(
fileName,
"application/pdf",
fileSize,
`documents/${req.user.id}`,
);
// Store pending upload in database
await db.documents.create({
reference: result.reference,
userId: req.user.id,
status: "pending",
});
res.json({
uploadUrl: result.uploadUrl,
reference: result.reference,
});
});
// Frontend confirms upload complete
app.post("/documents/confirm-upload", async (req, res) => {
const { reference } = req.body;
const result = await storage.validateAndConfirmUpload(reference, {
expectedContentType: "application/pdf",
});
if (result.success) {
await db.documents.update(
{ reference },
{
status: "uploaded",
size: result.actualFileSize,
},
);
res.json({ success: true, viewUrl: result.viewUrl });
} else {
await db.documents.delete({ reference });
res.status(400).json({ error: result.error });
}
});
```
### Bulk File Upload
```typescript
app.post("/gallery/upload", upload.array("photos", 20), async (req, res) => {
const files = req.files as Express.Multer.File[];
const results = await storage.uploadFiles(files, {
maxSize: 10 * 1024 * 1024,
allowedMimeTypes: ["image/jpeg", "image/png"],
});
const successful = results.filter((r) => r.success);
const failed = results.filter((r) => !r.success);
res.json({
uploaded: successful.length,
failed: failed.length,
files: successful.map((r) => ({
reference: r.reference,
url: r.fileUrl,
})),
errors: failed.map((r) => r.error),
});
});
```
---
## Migrating Between Providers
Moving from local development to cloud production? Or switching cloud providers? Here's how.
### Local to S3
```env
# Before (development)
FILE_DRIVER=local
LOCAL_PATH=uploads
# After (production)
FILE_DRIVER=s3
BUCKET_NAME=my-app-uploads
AWS_REGION=us-east-1
```
Your code stays exactly the same. Files uploaded before migration remain in their original location — you'll need to migrate existing files separately if needed.
### S3 to Azure
```env
# Before
FILE_DRIVER=s3
BUCKET_NAME=my-bucket
AWS_REGION=us-east-1
# After
FILE_DRIVER=azure
BUCKET_NAME=my-container
AZURE_CONNECTION_STRING=DefaultEndpointsProtocol=https;AccountName=...
```
**Important**: If using presigned URLs, remember that Azure requires post-upload validation. Add `validateAndConfirmUpload()` calls to your confirmation endpoints.
---
## Migrating from v2 to v3
v3 has breaking changes in dependencies, types, and configuration. Most apps require minimal code changes.
### What Changed
1. **Cloud SDKs are optional peer dependencies.** Install only what you need — no more downloading all SDKs.
2. **Result types are discriminated unions.** `result.fileName` is guaranteed when `result.success === true`. Code that accessed properties without checking `success` may need updates.
3. **Presigned driver subclasses removed.** `S3PresignedStorageDriver`, `GCSPresignedStorageDriver`, and `AzurePresignedStorageDriver` are no longer exported. Use the base driver classes or `StorageManager` (the `'s3-presigned'` driver string still works).
4. **`rateLimit` option renamed to `rateLimiter`.** Now accepts either options or a custom adapter.
5. **`getRateLimitStatus()` is async.** Returns a Promise.
6. **`deleteFile()` returns `DeleteResult`** instead of `boolean`. Check `result.success` instead of the boolean value.
7. **`IStorageDriver.delete()` returns `DeleteResult`** instead of `boolean`. Custom drivers must be updated.
8. **`ensureDirectoryExists()` is async.** Returns a `Promise` — add `await` to existing calls.
9. **Presigned URL methods return stricter types.** `generateUploadUrl()` returns `PresignedUploadUrlResult` (guarantees `uploadUrl`, `fileName`, `reference`, `expiresIn` on success). `generateViewUrl()` returns `PresignedViewUrlResult` (guarantees `viewUrl`, `reference`, `expiresIn` on success).
### Migration Steps
1. Update the package:
```bash
npm install express-storage@3
```
2. Install the SDK for your provider:
```bash
# If you use S3
npm install @aws-sdk/client-s3 @aws-sdk/lib-storage @aws-sdk/s3-request-presigner
# If you use GCS
npm install @google-cloud/storage
# If you use Azure
npm install @azure/storage-blob @azure/identity
```
3. Update result type access — `fileName` is now `reference`:
```typescript
// Before (v2)
const name = result.fileName!;
// After (v3) — "reference" is the stored file path used for all subsequent operations
if (result.success) {
const ref = result.reference; // pass to deleteFile(), getMetadata(), generateViewUrl()
const url = result.fileUrl; // URL to access the file
}
```
4. Update rate limiting config (if used):
```typescript
// Before (v2)
new StorageManager({ driver: "s3", rateLimit: { maxRequests: 100 } });
// After (v3)
new StorageManager({ driver: "s3", rateLimiter: { maxRequests: 100 } });
```
If you forget to install a required SDK, you'll get a clear error message telling you exactly what to install.
---
## Why Express Storage over Alternatives?
If you're evaluating file upload libraries for Express.js, here's how Express Storage compares:
| Feature | **Express Storage** | **multer-s3** | **express-fileupload** | **uploadfs** |
| --------------------------- | ------------------- | ------------- | ---------------------- | ------------ |
| AWS S3 | Yes | Yes | Manual | Yes |
| Google Cloud Storage | Yes | No | No | Yes |
| Azure Blob Storage | Yes | No | No | Yes |
| Local disk | Yes | No | Yes | Yes |
| Presigned URLs | Yes | No | No | No |
| File validation | Yes | No | Partial | No |
| TypeScript (native) | Yes | No | @types | No |
| Streaming uploads | Yes | Yes | No | No |
| Switch providers at runtime | Yes (env var) | No | No | No |
| Path traversal protection | Yes | No | No | No |
| Lifecycle hooks | Yes | No | No | No |
| Batch operations | Yes | No | No | No |
| Rate limiting | Yes | No | No | No |
**multer-s3** is great if you only need S3. Express Storage covers S3 *plus* GCS, Azure, and local disk with the same code — and adds presigned URLs, validation, and security that multer-s3 doesn't provide.
---
## TypeScript Support
Express Storage is written in TypeScript and exports all types:
```typescript
// Core — what most users need
import {
StorageManager,
InMemoryRateLimiter,
FileUploadResult,
DeleteResult,
PresignedUploadUrlResult,
StorageOptions,
FileValidationOptions,
UploadOptions,
} from "express-storage";
// Utilities — standalone helpers (import separately to keep your bundle small)
import { withRetry, formatFileSize, withConcurrencyLimit } from "express-storage/utils";
// Drivers — for custom driver implementations or direct driver use
import { BaseStorageDriver, createDriver } from "express-storage/drivers";
// Config — environment variable loading and validation
import { validateStorageConfig, loadAndValidateConfig } from "express-storage/config";
// Discriminated unions — TypeScript narrows automatically
const result: FileUploadResult = await storage.uploadFile(file);
if (result.success) {
// TypeScript knows: result is FileUploadSuccess
console.log(result.reference); // string — stored file path
console.log(result.fileUrl); // string — URL to access
} else {
// TypeScript knows: result is FileUploadError
console.log(result.error); // string (guaranteed)
}
```
---
## Contributing
Contributions are welcome!
```bash
# Clone the repository
git clone https://github.com/th3hero/express-storage.git
# Install dependencies (includes all cloud SDKs for development)
npm install
# Run tests
npm test
# Run tests in watch mode
npm run test:watch
# Build for production
npm run build
# Run linting
npm run lint
```
---
## License
MIT License — use it however you want.
---
## Support
- **Issues**: [GitHub Issues](https://github.com/th3hero/express-storage/issues)
- **Author**: Alok Kumar ([@th3hero](https://github.com/th3hero))
---
**Made for developers who are tired of writing upload code from scratch.**