Skip to content

CdnDisk ​

CdnDisk provides CDN URL rewriting and CloudFront signed URL generation. Unlike other decorators, CdnDisk is automatically applied when a disk's configuration includes a cdn property. You do not need to manually wrap disks with it.

When to Use ​

  • Public asset delivery: serve images, CSS, JS through a CDN
  • CloudFront signed URLs: time-limited access to private content via CloudFront
  • Custom domain URLs: return https://cdn.example.com/path instead of S3 URLs
  • Edge caching: leverage CDN edge locations for global performance

Auto-Application ​

When you configure a disk with a cdn property, CdnDisk is automatically applied:

typescript
// storage.config.ts
StorageModule.forRoot({
  default: 's3',
  disks: {
    s3: {
      driver: 's3',
      bucket: 'my-assets',
      region: 'us-east-1',
      // This automatically wraps the S3 disk with CdnDisk
      cdn: {
        baseUrl: 'https://cdn.example.com',
        provider: 'generic',
      },
    },
  },
}),

Auto-Wrapping Behavior

You do not call storage.withCdn() or manually create a CdnDisk. The StorageModule detects the cdn config property and wraps the disk automatically during initialization. The disk you get from storage.disk('s3') is already a CdnDisk.

CdnConfig ​

OptionTypeRequiredDefaultDescription
baseUrlstringYes--The CDN base URL (e.g., https://d1234.cloudfront.net)
provider'cloudfront' | 'generic'No'generic'CDN provider. 'cloudfront' enables signed URL generation
signingKeyIdstringNo--CloudFront key pair ID (required when provider: 'cloudfront')
signingKeystringNo--CloudFront private key PEM (required when provider: 'cloudfront')

Generic CDN ​

For simple URL rewriting without signing:

typescript
StorageModule.forRoot({
  default: 's3',
  disks: {
    assets: {
      driver: 's3',
      bucket: 'public-assets',
      region: 'us-east-1',
      cdn: {
        baseUrl: 'https://cdn.myapp.com',
        provider: 'generic',
      },
    },
  },
}),
typescript
@Injectable()
export class AssetService {
  constructor(private readonly storage: StorageService) {}

  async getAssetUrl(path: string): Promise<string> {
    // Returns: https://cdn.myapp.com/images/hero.jpg
    // Instead of: https://public-assets.s3.amazonaws.com/images/hero.jpg
    return this.storage.disk('assets').url(path);
  }
}

CloudFront Signed URLs ​

For private content served through CloudFront with time-limited access:

Setup ​

  1. Create a CloudFront key pair in the AWS Console (CloudFront > Key Management > Public Keys)
  2. Get the key pair ID and download the private key PEM file
  3. Configure the disk:
typescript
StorageModule.forRoot({
  default: 's3',
  disks: {
    private: {
      driver: 's3',
      bucket: 'private-content',
      region: 'us-east-1',
      cdn: {
        baseUrl: 'https://d1234abcdef.cloudfront.net',
        provider: 'cloudfront',
        signingKeyId: process.env.CLOUDFRONT_KEY_ID,
        signingKey: process.env.CLOUDFRONT_PRIVATE_KEY,
      },
    },
  },
}),
  1. Install the optional peer dependency:
bash
pnpm add @aws-sdk/cloudfront-signer

Generating Signed URLs ​

typescript
@Injectable()
export class PrivateContentService {
  constructor(private readonly storage: StorageService) {}

  async getSignedUrl(path: string): Promise<string> {
    // Returns a CloudFront signed URL valid for the specified duration
    // https://d1234abcdef.cloudfront.net/videos/lesson-1.mp4?Expires=...&Signature=...&Key-Pair-Id=...
    return this.storage.disk('private').temporaryUrl(path, {
      expiresIn: 3600, // 1 hour
    });
  }
}

CloudFront Signer Dependency

CloudFront signed URL generation requires the optional peer dependency @aws-sdk/cloudfront-signer. If this package is not installed and you attempt to generate a signed URL, a StorageConfigurationError will be thrown.

bash
pnpm add @aws-sdk/cloudfront-signer

Full CloudFront Integration Example ​

typescript
import {
  Controller,
  Get,
  Post,
  Param,
  UploadedFile,
  UseInterceptors,
  Headers,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { StorageService } from '@fozooni/nestjs-storage';

@Controller('media')
export class MediaController {
  constructor(private readonly storage: StorageService) {}

  @Post('upload')
  @UseInterceptors(FileInterceptor('file'))
  async upload(@UploadedFile() file: Express.Multer.File) {
    const path = `media/${Date.now()}-${file.originalname}`;
    const disk = this.storage.disk('private');
    await disk.put(path, file.buffer);

    return {
      path,
      // Public CDN URL (for public assets)
      cdnUrl: await disk.url(path),
      // Signed URL (for private assets, expires in 1 hour)
      signedUrl: await disk.temporaryUrl(path, { expiresIn: 3600 }),
    };
  }

  @Get('stream/:path(*)')
  async getStreamUrl(@Param('path') path: string) {
    const disk = this.storage.disk('private');

    // Short-lived signed URL for video streaming
    const url = await disk.temporaryUrl(path, {
      expiresIn: 7200, // 2 hours for video streaming
    });

    return { url };
  }

  @Get('thumbnail/:path(*)')
  async getThumbnailUrl(@Param('path') path: string) {
    const disk = this.storage.disk('private');

    // Longer-lived URL for static thumbnails
    const url = await disk.temporaryUrl(path, {
      expiresIn: 86400, // 24 hours
    });

    return { url };
  }
}

URL Behavior ​

MethodGeneric CDNCloudFront CDN
url(path){baseUrl}/{path}{baseUrl}/{path} (unsigned)
temporaryUrl(path, opts)Falls through to inner disk's presigned URLCloudFront signed URL
presignedPost(path, opts)Falls through to inner diskFalls through to inner disk

Manual Construction ​

While auto-application is the standard approach, you can manually create a CdnDisk:

typescript
import { CdnDisk } from '@fozooni/nestjs-storage';

const innerDisk = storage.disk('s3');
const cdnDisk = new CdnDisk(innerDisk, {
  baseUrl: 'https://cdn.example.com',
  provider: 'generic',
});

const url = await cdnDisk.url('images/logo.png');
// https://cdn.example.com/images/logo.png

CDN Invalidation ​

CdnDisk exposes an invalidateCdn() placeholder method for cache invalidation:

typescript
// Note: This is a placeholder — actual CDN invalidation
// requires provider-specific API calls
await cdnDisk.invalidateCdn('/images/logo.png');

CDN Invalidation

The invalidateCdn() method is a placeholder for future CDN-specific invalidation implementations. For production cache invalidation, use the cloud provider's SDK directly (e.g., CloudFront.createInvalidation()).

Multi-CDN Configuration ​

Different disks can use different CDN providers:

typescript
StorageModule.forRoot({
  default: 'public',
  disks: {
    public: {
      driver: 's3',
      bucket: 'public-assets',
      cdn: {
        baseUrl: 'https://assets.example.com',
        provider: 'generic', // Simple URL rewriting
      },
    },
    premium: {
      driver: 's3',
      bucket: 'premium-content',
      cdn: {
        baseUrl: 'https://d1234.cloudfront.net',
        provider: 'cloudfront', // Signed URLs
        signingKeyId: process.env.CF_KEY_ID,
        signingKey: process.env.CF_PRIVATE_KEY,
      },
    },
    media: {
      driver: 'gcs',
      bucket: 'media-files',
      // No CDN — uses direct GCS URLs
    },
  },
}),
typescript
@Injectable()
export class UrlService {
  constructor(private readonly storage: StorageService) {}

  async getPublicUrl(path: string): Promise<string> {
    // https://assets.example.com/path
    return this.storage.disk('public').url(path);
  }

  async getPremiumUrl(path: string): Promise<string> {
    // CloudFront signed URL
    return this.storage.disk('premium').temporaryUrl(path, {
      expiresIn: 3600,
    });
  }

  async getMediaUrl(path: string): Promise<string> {
    // Direct GCS URL (no CDN)
    return this.storage.disk('media').url(path);
  }
}

How It Works Under the Hood ​

  1. Auto-detection: During module initialization, StorageModule checks each disk configuration for a cdn property. If found, the driver disk is wrapped with CdnDisk.

  2. URL rewriting: The url() method concatenates baseUrl + '/' + normalizedPath instead of generating a provider-specific URL.

  3. Signed URLs: When provider: 'cloudfront', temporaryUrl() dynamically imports @aws-sdk/cloudfront-signer and calls getSignedUrl() with the configured key pair.

  4. Passthrough: All non-URL methods (get, put, delete, etc.) are delegated directly to the inner disk without modification.

Gotchas ​

CDN for Public, Presigned for Private

Use CDN URLs (url()) for publicly accessible content and presigned URLs (temporaryUrl()) for access-controlled content. CloudFront signed URLs combine CDN performance with access control.

Base URL Trailing Slash

Do not include a trailing slash in baseUrl. The library handles path joining:

typescript
// Correct
cdn: { baseUrl: 'https://cdn.example.com' }

// Incorrect — will produce double slashes
cdn: { baseUrl: 'https://cdn.example.com/' }

Keep CloudFront Private Keys Secure

Never commit CloudFront private keys to source control. Store them in environment variables or a secrets manager. The private key grants the ability to generate signed URLs for any content in your distribution.

Cross-References ​

Released under the MIT License.