Image Upload & Thumbnails Tutorial · Module 04 of 11

Cloud Storage Integration

Move images to S3 (or Minio, GCS) instead of local disk. Generate signed URLs for secure, time-limited access. Stream uploads directly to S3 to save bandwidth. Delete objects when images are removed. By the end, images are safely stored in the cloud with proper access control.

~3–4 hrsIntermediateStorage focus
← Back to Module 04 overview
What You'll Have at the End

Definition of Done

  • AWS SDK integration (or Minio for local dev).
  • Stream uploads directly to S3 bucket.
  • Signed URLs: GET /images/:id/url/:size returns time-limited download link (15 min expiry).
  • Original + thumbnails stored in S3 with organized prefix structure.
  • DELETE /images/:id removes all objects from S3.
  • Public URLs for cached images (with CDN cache headers).
  • Cost: images stored in S3, no local disk space used.
The Steps

Build It

STEP 1

Set up S3 client and storage service

Install: npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner

Create src/storage/s3.ts:

import { S3Client, PutObjectCommand, DeleteObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';

export class S3Storage {
  private s3: S3Client;
  private bucket: string;

  constructor() {
    this.s3 = new S3Client({ region: process.env.AWS_REGION || 'us-east-1' });
    this.bucket = process.env.S3_BUCKET || 'images';
  }

  async uploadFile(key: string, buffer: Buffer, mimetype: string) {
    const command = new PutObjectCommand({
      Bucket: this.bucket,
      Key: key,
      Body: buffer,
      ContentType: mimetype
    });
    return this.s3.send(command);
  }

  async deleteFile(key: string) {
    const command = new DeleteObjectCommand({
      Bucket: this.bucket,
      Key: key
    });
    return this.s3.send(command);
  }

  async getSignedUrl(key: string, expirationSeconds = 900) {
    const command = new GetObjectCommand({
      Bucket: this.bucket,
      Key: key
    });
    return getSignedUrl(this.s3, command, { expiresIn: expirationSeconds });
  }
}