Claude
Skills
Sign in
Back

bknd-serve-files

Included with Lifetime
$97 forever

Use when serving uploaded files to users. Covers API-proxied file serving, direct storage URLs (S3/R2/Cloudinary), CDN configuration, public file URLs, caching headers, image optimization with Cloudinary, and serving files in frontend applications.

Image & Video

What this skill does


# Serve Files

Serve uploaded files from Bknd storage to users via API proxy or direct storage URLs.

## Prerequisites

- Media module enabled in Bknd config
- Storage adapter configured (S3, R2, Cloudinary, or local)
- Files uploaded via `bknd-file-upload` skill
- For CDN: storage provider with CDN support (S3/R2/Cloudinary)

## When to Use UI Mode

- Admin panel > Media section > view/preview files
- Copy file URLs from admin interface
- Quick verification that files are accessible

## When to Use Code Mode

- Build file URLs programmatically
- Configure CDN or custom domains
- Implement image optimization
- Access control for file downloads

## File Serving Methods

Bknd supports two approaches to serve files:

| Method | Use Case | Performance | Control |
|--------|----------|-------------|---------|
| API Proxy | Simple setup, private files | Moderate | Full (auth, permissions) |
| Direct URL | High traffic, public files | Best (CDN) | Limited (bucket ACLs) |

## Step-by-Step: API Proxy (via Bknd)

Files served through Bknd API at `/api/media/file/{filename}`.

### Step 1: Get File URL

```typescript
import { Api } from "bknd";

const api = new Api({ host: "http://localhost:7654" });

// Build file URL
const fileUrl = `${api.host}/api/media/file/image.png`;
// "http://localhost:7654/api/media/file/image.png"
```

### Step 2: Display in Frontend

```tsx
function Image({ filename }) {
  const { api } = useApp();
  const src = `${api.host}/api/media/file/${filename}`;

  return <img src={src} alt="" />;
}
```

### Step 3: Download File (SDK)

```typescript
// Get as File object
const file = await api.media.download("image.png");

// Get as stream (for large files)
const stream = await api.media.getFileStream("image.png");
```

### Step 4: Verify Access

```bash
# Test file access
curl -I http://localhost:7654/api/media/file/image.png

# Response includes:
# Content-Type: image/png
# Content-Length: 12345
# ETag: "abc123..."
```

## Step-by-Step: Direct Storage URLs

Serve files directly from S3/R2/Cloudinary for better performance.

### S3/R2 Direct URLs

```typescript
// S3 URL pattern
const s3Url = `https://${bucket}.s3.${region}.amazonaws.com/${filename}`;
// "https://mybucket.s3.us-east-1.amazonaws.com/image.png"

// R2 URL pattern (public bucket)
const r2Url = `https://${customDomain}/${filename}`;
// "https://media.myapp.com/image.png"
```

### Cloudinary Direct URLs

Cloudinary provides automatic CDN and transformations:

```typescript
// Basic URL
const cloudinaryUrl = `https://res.cloudinary.com/${cloudName}/image/upload/${filename}`;

// With transformations
const optimizedUrl = `https://res.cloudinary.com/${cloudName}/image/upload/w_800,q_auto,f_auto/${filename}`;
```

### Building URLs in Code

```typescript
// Helper to get direct URL based on adapter type
function getFileUrl(filename: string, config: MediaConfig): string {
  const { adapter } = config;

  switch (adapter.type) {
    case "s3":
      // S3/R2 URL from configured endpoint
      return `${adapter.config.url}/${filename}`;

    case "cloudinary":
      return `https://res.cloudinary.com/${adapter.config.cloud_name}/image/upload/${filename}`;

    case "local":
      // Always use API proxy for local
      return `/api/media/file/${filename}`;

    default:
      return `/api/media/file/${filename}`;
  }
}
```

## CDN Configuration

### Cloudflare R2 with Custom Domain

1. **Create R2 bucket** in Cloudflare dashboard

2. **Enable public access** on bucket

3. **Configure custom domain** (Cloudflare DNS):
   - Add CNAME: `media.yourapp.com` -> `<bucket>.<account>.r2.dev`

4. **Use in Bknd config:**

```typescript
export default defineConfig({
  media: {
    enabled: true,
    adapter: {
      type: "s3",
      config: {
        access_key: process.env.R2_ACCESS_KEY,
        secret_access_key: process.env.R2_SECRET_KEY,
        url: `https://${process.env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com/${process.env.R2_BUCKET}`,
      },
    },
  },
});
```

5. **Serve files via custom domain:**

```typescript
const publicUrl = `https://media.yourapp.com/${filename}`;
```

### AWS S3 with CloudFront

1. **Create S3 bucket** with public read (or CloudFront OAI)

2. **Create CloudFront distribution:**
   - Origin: S3 bucket
   - Cache policy: CachingOptimized
   - Custom domain (optional)

3. **Use CloudFront URL:**

```typescript
const cdnUrl = `https://d123abc.cloudfront.net/${filename}`;
// Or with custom domain
const cdnUrl = `https://cdn.yourapp.com/${filename}`;
```

### Cloudinary (Built-in CDN)

Cloudinary includes global CDN automatically:

```typescript
export default defineConfig({
  media: {
    enabled: true,
    adapter: {
      type: "cloudinary",
      config: {
        cloud_name: "your-cloud-name",
        api_key: process.env.CLOUDINARY_API_KEY,
        api_secret: process.env.CLOUDINARY_API_SECRET,
      },
    },
  },
});
```

Files served from `res.cloudinary.com` with global CDN.

## Image Optimization

### Cloudinary Transformations

```typescript
// Build optimized image URL
function getOptimizedImage(filename: string, options: {
  width?: number;
  height?: number;
  quality?: "auto" | number;
  format?: "auto" | "webp" | "avif" | "jpg" | "png";
  crop?: "fill" | "fit" | "scale" | "thumb";
} = {}) {
  const cloudName = process.env.CLOUDINARY_CLOUD_NAME;
  const transforms: string[] = [];

  if (options.width) transforms.push(`w_${options.width}`);
  if (options.height) transforms.push(`h_${options.height}`);
  if (options.quality) transforms.push(`q_${options.quality}`);
  if (options.format) transforms.push(`f_${options.format}`);
  if (options.crop) transforms.push(`c_${options.crop}`);

  const transformStr = transforms.length > 0 ? transforms.join(",") + "/" : "";

  return `https://res.cloudinary.com/${cloudName}/image/upload/${transformStr}${filename}`;
}

// Usage
const thumb = getOptimizedImage("avatar.png", {
  width: 100,
  height: 100,
  crop: "fill",
  quality: "auto",
  format: "auto",
});
// "https://res.cloudinary.com/mycloud/image/upload/w_100,h_100,c_fill,q_auto,f_auto/avatar.png"
```

### Common Transformation Patterns

```typescript
// Responsive images
const srcSet = [400, 800, 1200].map(w =>
  `${getOptimizedImage(filename, { width: w, format: "auto" })} ${w}w`
).join(", ");

// Thumbnail generation
const thumb = getOptimizedImage(filename, {
  width: 150,
  height: 150,
  crop: "thumb",
});

// Automatic format (WebP/AVIF when supported)
const optimized = getOptimizedImage(filename, {
  quality: "auto",
  format: "auto",
});
```

## React Integration

### Image Component with Fallback

```tsx
function StoredImage({ filename, alt, ...props }) {
  const { api } = useApp();
  const [error, setError] = useState(false);

  // API proxy URL as fallback
  const apiUrl = `${api.host}/api/media/file/${filename}`;

  // Direct CDN URL (configure based on your adapter)
  const cdnUrl = `https://media.yourapp.com/${filename}`;

  return (
    <img
      src={error ? apiUrl : cdnUrl}
      alt={alt}
      onError={() => setError(true)}
      {...props}
    />
  );
}
```

### Responsive Image Component

```tsx
function ResponsiveImage({ filename, alt, sizes = "100vw" }) {
  const cloudName = process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME;
  const base = `https://res.cloudinary.com/${cloudName}/image/upload`;

  const srcSet = [400, 800, 1200, 1600].map(w =>
    `${base}/w_${w},q_auto,f_auto/${filename} ${w}w`
  ).join(", ");

  return (
    <img
      src={`${base}/w_800,q_auto,f_auto/${filename}`}
      srcSet={srcSet}
      sizes={sizes}
      alt={alt}
      loading="lazy"
    />
  );
}
```

### File Download Button

```tsx
function DownloadButton({ filename, label }) {
  const { api } = useApp();
  const [downloading, setDownloading] = useState(false);

  const handleDownload = async () => {
    setDownloading(true);
    try {
      const file = await api.media.download(filename);

      // Create download link
      const url = URL.createObject

Related in Image & Video