Backblaze B2 Driver
The B2 driver provides integration with Backblaze B2 Cloud Storage via its S3-compatible API. The driver extends S3Disk, so all S3 features are available. B2 is known for its extremely cost-effective storage pricing, making it an excellent choice for backups, archives, and large media libraries.
Installation
pnpm add @aws-sdk/client-s3 @aws-sdk/s3-request-presignerConfiguration
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
driver | 'b2' | Yes | — | Must be 'b2' |
bucket | string | Yes | — | B2 bucket name |
endpoint | string | Yes | — | B2 S3-compatible endpoint URL |
key | string | Yes | — | B2 application key ID |
secret | string | Yes | — | B2 application key |
region | string | Yes | — | B2 region (e.g., 'us-west-004') |
url | string | No | — | Custom base URL for public file URLs |
visibility | 'public' | 'private' | No | 'private' | Default visibility for new objects |
B2 S3-Compatible Endpoint URL Format
The B2 S3-compatible endpoint follows this pattern:
https://s3.<region>.backblazeb2.comExamples by region:
| Region | Endpoint |
|---|---|
us-west-004 | https://s3.us-west-004.backblazeb2.com |
us-west-002 | https://s3.us-west-002.backblazeb2.com |
eu-central-003 | https://s3.eu-central-003.backblazeb2.com |
You can find your bucket's region in the B2 Cloud Storage dashboard under the bucket details.
Basic Setup
import { Module } from '@nestjs/common';
import { StorageModule } from '@fozooni/nestjs-storage';
@Module({
imports: [
StorageModule.forRoot({
default: 'b2',
disks: {
b2: {
driver: 'b2',
bucket: 'my-app-backups',
endpoint: 'https://s3.us-west-004.backblazeb2.com',
region: 'us-west-004',
key: process.env.B2_KEY_ID,
secret: process.env.B2_APP_KEY,
},
},
}),
],
})
export class AppModule {}Async Configuration with ConfigService
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { StorageModule } from '@fozooni/nestjs-storage';
@Module({
imports: [
ConfigModule.forRoot(),
StorageModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
default: 'b2',
disks: {
b2: {
driver: 'b2',
bucket: config.getOrThrow('B2_BUCKET'),
endpoint: config.getOrThrow('B2_ENDPOINT'),
region: config.getOrThrow('B2_REGION'),
key: config.getOrThrow('B2_KEY_ID'),
secret: config.getOrThrow('B2_APP_KEY'),
},
},
}),
}),
],
})
export class AppModule {}Application Key Setup
B2 uses Application Keys for S3-compatible API access:
- Log into the Backblaze B2 dashboard
- Navigate to App Keys in the sidebar
- Click Add a New Application Key
- Configure the key:
- Name: A descriptive name (e.g.,
nestjs-storage-prod) - Bucket: Restrict to a specific bucket or allow access to all buckets
- Capabilities: Select required permissions (read, write, list, delete)
- File name prefix: Optionally restrict access to a key prefix
- Duration: Optionally set an expiration
- Name: A descriptive name (e.g.,
- Copy the keyID (use as
key) and applicationKey (use assecret)
WARNING
The application key secret is only shown once at creation time. Store it securely immediately. If lost, you must create a new key.
B2 Extends S3Disk
B2's S3-compatible API means the driver extends S3Disk:
S3Disk
└── B2Disk (endpoint: https://s3.<region>.backblazeb2.com)All features from the S3 driver page work with B2:
- Presigned URLs via
temporaryUrl()andtemporaryUploadUrl() - Presigned POST via
presignedPost() - Multipart uploads via
initMultipartUpload()/putFileMultipart() - Range requests via
getRange() - Conditional writes via
putIfMatch()/putIfNoneMatch() - Streaming via
getStream()/putStream() - Custom metadata
Cost-Effective Storage
B2 pricing is significantly lower than most cloud providers:
| B2 | S3 Standard | GCS Standard | |
|---|---|---|---|
| Storage | $0.006/GB/mo | $0.023/GB/mo | $0.020/GB/mo |
| Download | $0.01/GB | $0.09/GB | $0.12/GB |
| Class A ops (writes) | $0.004/10K | $0.005/1K | $0.005/1K |
| Class B ops (reads) | $0.004/10K | $0.0004/1K | $0.0004/1K |
| Free egress | 3x stored data/mo | None | None |
Prices as of 2024. Check provider pricing pages for current rates.
This makes B2 ideal for:
- Backups and archives — bulk storage at minimal cost
- Media libraries — large video/image collections
- Log storage — high-volume write, infrequent read
- Cold data — data retained for compliance but rarely accessed
File Operations
@Injectable()
export class B2BackupService {
constructor(private readonly storage: StorageService) {}
async storeBackup(name: string, data: Buffer) {
const disk = this.storage.disk('b2');
const key = `backups/${new Date().toISOString().split('T')[0]}/${name}`;
await disk.put(key, data, {
metadata: {
type: 'database-backup',
createdAt: new Date().toISOString(),
},
});
return { key, size: data.length };
}
async retrieveBackup(key: string): Promise<Buffer> {
const disk = this.storage.disk('b2');
return disk.get(key);
}
async listBackups(date?: string) {
const disk = this.storage.disk('b2');
const prefix = date ? `backups/${date}/` : 'backups/';
return disk.listContents(prefix, { deep: true });
}
async deleteOldBackups(olderThan: Date) {
const disk = this.storage.disk('b2');
const all = await disk.listContents('backups/', { deep: true });
for (const item of all) {
const meta = await disk.getMetadata(item.path);
if (meta.lastModified && meta.lastModified < olderThan) {
await disk.delete(item.path);
}
}
}
}Presigned URLs
@Injectable()
export class B2DownloadService {
constructor(private readonly storage: StorageService) {}
async getTemporaryDownloadLink(path: string): Promise<string> {
const disk = this.storage.disk('b2');
return disk.temporaryUrl(path, 3600); // 1 hour
}
async getClientUploadUrl(path: string): Promise<string> {
const disk = this.storage.disk('b2');
return disk.temporaryUploadUrl(path, 3600, {
contentType: 'application/octet-stream',
});
}
}Multipart Uploads
B2 supports multipart uploads for files larger than 100 MB (recommended for files over 200 MB):
import { createReadStream } from 'fs';
@Injectable()
export class B2LargeUploadService {
constructor(private readonly storage: StorageService) {}
async uploadLargeFile(localPath: string, remotePath: string) {
const disk = this.storage.disk('b2');
await disk.putFileMultipart(remotePath, createReadStream(localPath), {
partSize: 100 * 1024 * 1024, // 100 MB parts (B2 minimum is 5 MB)
concurrency: 3,
});
}
}TIP
B2 charges for incomplete multipart uploads. If an upload fails, make sure to abort it with abortMultipartUpload(). Consider setting B2 lifecycle rules to auto-clean incomplete uploads.
Streaming
@Controller('media')
export class B2MediaController {
constructor(private readonly storage: StorageService) {}
@Get(':path(*)')
@RangeServe()
async serve(@Param('path') path: string) {
return { disk: 'b2', path: `media/${path}` };
}
}B2 + Cloudflare CDN
B2 has a partnership with Cloudflare that enables free egress when serving B2 files through Cloudflare's CDN:
- Point a Cloudflare-proxied domain at your B2 bucket's friendly URL
- Set up a Cloudflare Worker or Page Rule to proxy requests to B2
StorageModule.forRoot({
default: 'b2',
disks: {
b2: {
driver: 'b2',
bucket: 'my-media',
endpoint: 'https://s3.us-west-004.backblazeb2.com',
region: 'us-west-004',
key: process.env.B2_KEY_ID,
secret: process.env.B2_APP_KEY,
url: 'https://media.example.com', // Cloudflare-proxied domain
},
},
})Complete Example: Media Archive
import { Injectable, NotFoundException, Logger } from '@nestjs/common';
import { StorageService } from '@fozooni/nestjs-storage';
import { v4 as uuid } from 'uuid';
@Injectable()
export class MediaArchiveService {
private readonly logger = new Logger(MediaArchiveService.name);
constructor(private readonly storage: StorageService) {}
private get disk() {
return this.storage.disk('b2');
}
async archive(file: Express.Multer.File, category: string) {
const id = uuid();
const ext = file.originalname.split('.').pop();
const key = `archive/${category}/${id}.${ext}`;
await this.disk.put(key, file.buffer, {
metadata: {
originalName: file.originalname,
category,
archivedAt: new Date().toISOString(),
},
});
this.logger.log(`Archived: ${key} (${file.size} bytes)`);
return { id, key };
}
async retrieve(key: string): Promise<{ url: string }> {
if (!(await this.disk.exists(key))) {
throw new NotFoundException('Archive item not found');
}
const url = await this.disk.temporaryUrl(key, 7200); // 2 hours
return { url };
}
}Environment Variables Example
# .env
B2_BUCKET=my-app-backups
B2_ENDPOINT=https://s3.us-west-004.backblazeb2.com
B2_REGION=us-west-004
B2_KEY_ID=your-b2-key-id
B2_APP_KEY=your-b2-application-key