Skip to main content
Storage drivers can control how URLs are generated for stored files. You can use URL builders to implement CDN, custom domains, Signed URLs, and more.

What is a URL Builder?

A URL builder is a function that converts a file key to a URL.
{
  urlBuilder: {
    generateURL: (key: string) => string
  }
}
Example:
key: 'uploads/avatar.png'
'/uploads/avatar.png'  (fs)
'https://bucket.s3.amazonaws.com/uploads/avatar.png'  (s3)
'https://cdn.example.com/uploads/avatar.png'  (CDN)

fs Driver URL Builder

The fs driver requires urlBuilder configuration.

Basic Configuration

import { drivers } from "sonamu";

storage: {
  default: 'fs',
  drivers: {
    fs: drivers.fs({
      location: './uploads',
      urlBuilder: {
        generateURL: (key) => `/uploads/${key}`,
      }
    }),
  }
}
Result:
// key: 'avatars/user-1.png'
// location: './uploads'
// File path: ./uploads/avatars/user-1.png
// URL: /uploads/avatars/user-1.png

Custom Domain

storage: {
  default: 'fs',
  drivers: {
    fs: drivers.fs({
      location: './uploads',
      urlBuilder: {
        generateURL: (key) =>
          `https://files.example.com/uploads/${key}`,
      }
    }),
  }
}
Result:
// key: 'avatars/user-1.png'
// URL: https://files.example.com/uploads/avatars/user-1.png
Use case: Separate file server or CDN

Static File Serving

When using the fs driver, the Fastify static plugin is required.
import path from "path";
import { type SonamuConfig } from "sonamu";

export const config: SonamuConfig = {
  server: {
    // Static file serving
    plugins: {
      static: {
        root: path.join(process.cwd(), 'uploads'),
        prefix: '/uploads/',
      }
    },

    // Storage configuration
    storage: {
      default: 'fs',
      drivers: {
        fs: drivers.fs({
          location: './uploads',
          urlBuilder: {
            generateURL: (key) => `/uploads/${key}`,
          }
        }),
      }
    }
  }
};
Effect:
http://localhost:3000/uploads/avatars/user-1.png
→ Serves ./uploads/avatars/user-1.png file

S3 Driver URL Builder

The S3 driver generates S3 URLs by default, but can be customized with urlBuilder.

Default Behavior (without urlBuilder)

storage: {
  default: 's3',
  drivers: {
    s3: drivers.s3({
      credentials: { /* ... */ },
      region: 'ap-northeast-2',
      bucket: 'my-bucket',
      // No urlBuilder
    }),
  }
}
Result:
// key: 'uploads/avatar.png'
// URL: https://my-bucket.s3.ap-northeast-2.amazonaws.com/uploads/avatar.png

CloudFront CDN

To serve files through CDN:
storage: {
  default: 's3',
  drivers: {
    s3: drivers.s3({
      credentials: { /* ... */ },
      region: 'ap-northeast-2',
      bucket: 'my-bucket',
      urlBuilder: {
        generateURL: (key) =>
          `https://d111111abcdef8.cloudfront.net/${key}`,
      }
    }),
  }
}
CLOUDFRONT_DOMAIN=d111111abcdef8.cloudfront.net
Benefits:
  • Fast transfer speed (edge locations)
  • Reduced bandwidth costs
  • Caching

Custom Domain

storage: {
  default: 's3',
  drivers: {
    s3: drivers.s3({
      credentials: { /* ... */ },
      region: 'ap-northeast-2',
      bucket: 'my-bucket',
      urlBuilder: {
        generateURL: (key) =>
          `https://cdn.example.com/${key}`,
      }
    }),
  }
}
Configuration steps:
  1. Create CloudFront distribution
  2. Origin: S3 bucket
  3. CNAME: cdn.example.com
  4. DNS: cdn.example.com → CloudFront

Dynamic URL Generation

You can use environment variables or logic in the URL builder.

Environment-based URL

const baseUrl = process.env.NODE_ENV === 'production'
  ? 'https://cdn.example.com'
  : 'http://localhost:3000/uploads';

storage: {
  default: 'fs',
  drivers: {
    fs: drivers.fs({
      location: './uploads',
      urlBuilder: {
        generateURL: (key) => `${baseUrl}/${key}`,
      }
    }),
  }
}
Result:
// Development: http://localhost:3000/uploads/avatar.png
// Production: https://cdn.example.com/avatar.png

Path Transformation

storage: {
  default: 'fs',
  drivers: {
    fs: drivers.fs({
      location: './uploads',
      urlBuilder: {
        generateURL: (key) => {
          // Convert spaces to hyphens
          const safeKey = key.replace(/\s+/g, '-');
          return `/uploads/${safeKey}`;
        }
      }
    }),
  }
}

Version Management

const version = 'v1';

storage: {
  default: 'fs',
  drivers: {
    fs: drivers.fs({
      location: './uploads',
      urlBuilder: {
        generateURL: (key) => `/uploads/${version}/${key}`,
      }
    }),
  }
}
Result:
// URL: /uploads/v1/avatars/user-1.png

Signed URL (S3)

S3 can generate temporary access URLs.

getSignedUrl()

import { Sonamu } from "sonamu";

@api({ httpMethod: 'GET' })
async getPrivateFileUrl(key: string) {
  const disk = Sonamu.storage.use('s3');

  // Generate Signed URL (valid for 1 hour)
  const signedUrl = await disk.getSignedUrl(key, {
    expiresIn: '1h'
  });

  return { url: signedUrl };
}
Result:
https://bucket.s3.amazonaws.com/private/doc.pdf?X-Amz-Algorithm=...&X-Amz-Expires=3600
Use cases:
  • Temporary sharing of private files
  • Secure downloads
  • Time-limited access

UploadedFile’s signedUrl

@upload({ mode: 'single' })
@api({ httpMethod: 'POST' })
async uploadPrivateFile() {
  const { files } = Sonamu.getContext();
  const file = files?.[0]; // Use first file

  if (!file) {
    throw new Error('No file provided');
  }

  // Save
  await file.saveToDisk('private/doc.pdf', 's3');

  return {
    url: file.url,           // Unsigned URL
    signedUrl: file.signedUrl  // Signed URL
  };
}

Practical Examples

1. CDN + Version Management

const cdnDomain = process.env.CDN_DOMAIN;
const version = process.env.ASSET_VERSION || 'v1';

export const config: SonamuConfig = {
  server: {
    storage: {
      default: 's3',
      drivers: {
        s3: drivers.s3({
          credentials: {
            accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
            secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
          },
          region: 'ap-northeast-2',
          bucket: 'my-bucket',
          urlBuilder: {
            generateURL: (key) =>
              `https://${cdnDomain}/${version}/${key}`,
          }
        }),
      }
    }
  }
};
CDN_DOMAIN=d111111abcdef8.cloudfront.net
ASSET_VERSION=v2
Result:
// key: 'images/logo.png'
// URL: https://d111111abcdef8.cloudfront.net/v2/images/logo.png

2. Multiple CDNs

export const config: SonamuConfig = {
  server: {
    storage: {
      default: 's3',
      drivers: {
        // CDN for images
        images: drivers.s3({
          credentials: { /* ... */ },
          region: 'ap-northeast-2',
          bucket: 'images-bucket',
          urlBuilder: {
            generateURL: (key) =>
              `https://images.cdn.example.com/${key}`,
          }
        }),

        // CDN for videos
        videos: drivers.s3({
          credentials: { /* ... */ },
          region: 'ap-northeast-2',
          bucket: 'videos-bucket',
          urlBuilder: {
            generateURL: (key) =>
              `https://videos.cdn.example.com/${key}`,
          }
        }),
      }
    }
  }
};
Usage:
// Images
await file.saveToDisk('photo.jpg', 'images');
// URL: https://images.cdn.example.com/photo.jpg

// Videos
await file.saveToDisk('video.mp4', 'videos');
// URL: https://videos.cdn.example.com/video.mp4

3. Hash-based URL

import { createHash } from "crypto";

storage: {
  default: 's3',
  drivers: {
    s3: drivers.s3({
      credentials: { /* ... */ },
      region: 'ap-northeast-2',
      bucket: 'my-bucket',
      urlBuilder: {
        generateURL: (key) => {
          // Generate hash from key (cache busting)
          const hash = createHash('md5')
            .update(key)
            .digest('hex')
            .substring(0, 8);

          return `https://cdn.example.com/${key}?v=${hash}`;
        }
      }
    }),
  }
}
Result:
// key: 'images/logo.png'
// URL: https://cdn.example.com/images/logo.png?v=5d41402a

4. Conditional URL

storage: {
  default: 's3',
  drivers: {
    s3: drivers.s3({
      credentials: { /* ... */ },
      region: 'ap-northeast-2',
      bucket: 'my-bucket',
      urlBuilder: {
        generateURL: (key) => {
          // Images through CDN, others direct from S3
          if (key.match(/\.(jpg|jpeg|png|gif|webp)$/i)) {
            return `https://cdn.example.com/${key}`;
          }
          return `https://my-bucket.s3.amazonaws.com/${key}`;
        }
      }
    }),
  }
}

5. Resizing Service

storage: {
  default: 's3',
  drivers: {
    s3: drivers.s3({
      credentials: { /* ... */ },
      region: 'ap-northeast-2',
      bucket: 'my-bucket',
      urlBuilder: {
        generateURL: (key) => {
          // Pass through image resizing service
          if (key.match(/\.(jpg|jpeg|png)$/i)) {
            return `https://img.example.com/resize/800x600/${key}`;
          }
          return `https://cdn.example.com/${key}`;
        }
      }
    }),
  }
}

URL Types

Sonamu Storage provides two types of URLs.

Unsigned URL

Regular public URL
const url = await disk.getUrl('uploads/avatar.png');
// => https://bucket.s3.amazonaws.com/uploads/avatar.png
Characteristics:
  • Permanent
  • No expiration
  • For public files

Signed URL (S3)

Signed temporary URL
const signedUrl = await disk.getSignedUrl('private/doc.pdf', {
  expiresIn: '1h'  // Expires in 1 hour
});
// => https://bucket.s3.amazonaws.com/private/doc.pdf?X-Amz-Algorithm=...
Characteristics:
  • Temporary (with expiration time)
  • Protected by signature
  • For private files
Expiration time options:
{ expiresIn: '1h' }    // 1 hour
{ expiresIn: '30m' }   // 30 minutes
{ expiresIn: '1d' }    // 1 day
{ expiresIn: '7d' }    // 7 days

Cautions

Cautions when using URL builders:
  1. fs requires urlBuilder: fs driver requires urlBuilder configuration
    // ❌ No urlBuilder
    fs: drivers.fs({
      location: './uploads',
    })
    
    // ✅ urlBuilder configured
    fs: drivers.fs({
      location: './uploads',
      urlBuilder: {
        generateURL: (key) => `/uploads/${key}`,
      }
    })
    
  2. Static file serving: static plugin required when using fs
    plugins: {
      static: {
        root: path.join(process.cwd(), 'uploads'),
        prefix: '/uploads/',
      }
    }
    
  3. Use HTTPS: Use HTTPS URLs in production
    // ❌ HTTP
    generateURL: (key) => `http://cdn.example.com/${key}`
    
    // ✅ HTTPS
    generateURL: (key) => `https://cdn.example.com/${key}`
    
  4. Path encoding: Handle special characters
    // ✅ Safe handling
    generateURL: (key) => {
      const encoded = encodeURIComponent(key);
      return `/uploads/${encoded}`;
    }
    
  5. Cache invalidation: Consider CDN cache when changing URLs
    // Add version parameter for cache invalidation
    generateURL: (key) =>
      `/uploads/${key}?v=${Date.now()}`
    

CloudFront Configuration

How to use CloudFront with URL builders.

1. Create CloudFront Distribution

  1. AWS Console → CloudFront
  2. Click “Create Distribution”
  3. Origin:
    • Origin Domain: my-bucket.s3.ap-northeast-2.amazonaws.com
    • Origin Path: (leave empty)
  4. Default Cache Behavior:
    • Viewer Protocol Policy: Redirect HTTP to HTTPS
    • Allowed HTTP Methods: GET, HEAD, OPTIONS
  5. Create Distribution

2. Check Domain

After creating the distribution, check the Domain Name:
d111111abcdef8.cloudfront.net

3. Configure URL Builder

storage: {
  default: 's3',
  drivers: {
    s3: drivers.s3({
      credentials: { /* ... */ },
      region: 'ap-northeast-2',
      bucket: 'my-bucket',
      urlBuilder: {
        generateURL: (key) =>
          `https://d111111abcdef8.cloudfront.net/${key}`,
      }
    }),
  }
}

4. Custom Domain (Optional)

  1. Create CNAME record in Route 53:
    cdn.example.com → d111111abcdef8.cloudfront.net
    
  2. CloudFront distribution settings:
    • Alternate Domain Names (CNAMEs): cdn.example.com
    • SSL Certificate: Add certificate
  3. Update URL builder:
    urlBuilder: {
      generateURL: (key) =>
        `https://cdn.example.com/${key}`,
    }
    

Next Steps