Skip to content

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

bash
pnpm add @aws-sdk/client-s3 @aws-sdk/s3-request-presigner

Configuration

FieldTypeRequiredDefaultDescription
driver'b2'YesMust be 'b2'
bucketstringYesB2 bucket name
endpointstringYesB2 S3-compatible endpoint URL
keystringYesB2 application key ID
secretstringYesB2 application key
regionstringYesB2 region (e.g., 'us-west-004')
urlstringNoCustom 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.com

Examples by region:

RegionEndpoint
us-west-004https://s3.us-west-004.backblazeb2.com
us-west-002https://s3.us-west-002.backblazeb2.com
eu-central-003https://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

typescript
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

typescript
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:

  1. Log into the Backblaze B2 dashboard
  2. Navigate to App Keys in the sidebar
  3. Click Add a New Application Key
  4. 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
  5. Copy the keyID (use as key) and applicationKey (use as secret)

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() and temporaryUploadUrl()
  • 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:

B2S3 StandardGCS 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 egress3x stored data/moNoneNone

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

typescript
@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

typescript
@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):

typescript
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

typescript
@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:

  1. Point a Cloudflare-proxied domain at your B2 bucket's friendly URL
  2. Set up a Cloudflare Worker or Page Rule to proxy requests to B2
typescript
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

typescript
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

bash
# .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

Released under the MIT License.