> ## Documentation Index
> Fetch the complete documentation index at: https://sonamu.cartanova.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# URL Builder

> File URL generation and customization

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.

```typescript theme={null}
{
  urlBuilder: {
    generateURL: (key: string) => string
  }
}
```

**Example**:

```typescript theme={null}
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

```typescript theme={null}
import { drivers } from "sonamu";

storage: {
  default: 'fs',
  drivers: {
    fs: drivers.fs({
      location: './uploads',
      urlBuilder: {
        generateURL: (key) => `/uploads/${key}`,
      }
    }),
  }
}
```

**Result**:

```typescript theme={null}
// key: 'avatars/user-1.png'
// location: './uploads'
// File path: ./uploads/avatars/user-1.png
// URL: /uploads/avatars/user-1.png
```

### Custom Domain

```typescript theme={null}
storage: {
  default: 'fs',
  drivers: {
    fs: drivers.fs({
      location: './uploads',
      urlBuilder: {
        generateURL: (key) =>
          `https://files.example.com/uploads/${key}`,
      }
    }),
  }
}
```

**Result**:

```typescript theme={null}
// 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.

```typescript theme={null}
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)

```typescript theme={null}
storage: {
  default: 's3',
  drivers: {
    s3: drivers.s3({
      credentials: { /* ... */ },
      region: 'ap-northeast-2',
      bucket: 'my-bucket',
      // No urlBuilder
    }),
  }
}
```

**Result**:

```typescript theme={null}
// key: 'uploads/avatar.png'
// URL: https://my-bucket.s3.ap-northeast-2.amazonaws.com/uploads/avatar.png
```

### CloudFront CDN

To serve files through CDN:

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

```.env theme={null}
CLOUDFRONT_DOMAIN=d111111abcdef8.cloudfront.net
```

**Benefits**:

* Fast transfer speed (edge locations)
* Reduced bandwidth costs
* Caching

### Custom Domain

```typescript theme={null}
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

```typescript theme={null}
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**:

```typescript theme={null}
// Development: http://localhost:3000/uploads/avatar.png
// Production: https://cdn.example.com/avatar.png
```

### Path Transformation

```typescript theme={null}
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

```typescript theme={null}
const version = 'v1';

storage: {
  default: 'fs',
  drivers: {
    fs: drivers.fs({
      location: './uploads',
      urlBuilder: {
        generateURL: (key) => `/uploads/${version}/${key}`,
      }
    }),
  }
}
```

**Result**:

```typescript theme={null}
// URL: /uploads/v1/avatars/user-1.png
```

## Signed URL (S3)

S3 can generate **temporary access URLs**.

### getSignedUrl()

```typescript theme={null}
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

```typescript theme={null}
@upload()
async uploadPrivateFile() {
  const { bufferedFiles } = Sonamu.getContext();
  const file = bufferedFiles?.[0]; // Use first file

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

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

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

## Practical Examples

### 1. CDN + Version Management

```typescript theme={null}
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}`,
          }
        }),
      }
    }
  }
};
```

```.env theme={null}
CDN_DOMAIN=d111111abcdef8.cloudfront.net
ASSET_VERSION=v2
```

**Result**:

```typescript theme={null}
// key: 'images/logo.png'
// URL: https://d111111abcdef8.cloudfront.net/v2/images/logo.png
```

### 2. Multiple CDNs

```typescript theme={null}
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**:

```typescript theme={null}
// Images
await file.saveToDisk('images', 'photo.jpg');
// URL: https://images.cdn.example.com/photo.jpg

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

### 3. Hash-based URL

```typescript theme={null}
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**:

```typescript theme={null}
// key: 'images/logo.png'
// URL: https://cdn.example.com/images/logo.png?v=5d41402a
```

### 4. Conditional URL

```typescript theme={null}
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

```typescript theme={null}
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**

```typescript theme={null}
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**

```typescript theme={null}
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**:

```typescript theme={null}
{ expiresIn: '1h' }    // 1 hour
{ expiresIn: '30m' }   // 30 minutes
{ expiresIn: '1d' }    // 1 day
{ expiresIn: '7d' }    // 7 days
```

## Cautions

<Warning>
  **Cautions when using URL builders**:

  1. **fs requires urlBuilder**: fs driver requires urlBuilder configuration
     ```typescript theme={null}
     // ❌ 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
     ```typescript theme={null}
     plugins: {
       static: {
         root: path.join(process.cwd(), 'uploads'),
         prefix: '/uploads/',
       }
     }
     ```

  3. **Use HTTPS**: Use HTTPS URLs in production
     ```typescript theme={null}
     // ❌ HTTP
     generateURL: (key) => `http://cdn.example.com/${key}`

     // ✅ HTTPS
     generateURL: (key) => `https://cdn.example.com/${key}`
     ```

  4. **Path encoding**: Handle special characters
     ```typescript theme={null}
     // ✅ Safe handling
     generateURL: (key) => {
       const encoded = encodeURIComponent(key);
       return `/uploads/${encoded}`;
     }
     ```

  5. **Cache invalidation**: Consider CDN cache when changing URLs
     ```typescript theme={null}
     // Add version parameter for cache invalidation
     generateURL: (key) =>
       `/uploads/${key}?v=${Date.now()}`
     ```
</Warning>

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

```typescript theme={null}
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:
   ```typescript theme={null}
   urlBuilder: {
     generateURL: (key) =>
       `https://cdn.example.com/${key}`,
   }
   ```

## Next Steps

<CardGroup cols={2}>
  <Card title="Storage Manager" icon="database" href="/en/advanced-features/storage/storage-manager">
    Managing multiple disks
  </Card>

  <Card title="Storage Drivers" icon="hard-drive" href="/en/advanced-features/storage/storage-drivers">
    fs, S3 driver configuration
  </Card>

  <Card title="File Operations" icon="upload" href="/en/advanced-features/storage/file-operations">
    File upload and download
  </Card>
</CardGroup>
