API Reference/Configuration

Path Configuration

Complete guide to organizing your uploads with hierarchical path structures and custom file organization

Path Configuration

Organize your uploads with powerful hierarchical path structures that provide clean file organization, prevent conflicts, and enable scalable storage patterns.

New in v2.0: The hierarchical path system allows global configuration to provide the foundation while route-level paths extend and nest within it - no more overrides that lose configuration!

How Hierarchical Paths Work

The path system works in layers that build upon each other:

Path Structure: {globalPrefix}/{routePrefix}/{globalGenerated}

  • Global prefix: uploads (foundation for all files)
  • Route prefix: images, documents (category organization)
  • Global generated: {userId}/{timestamp}/{randomId}/{filename} (file structure)

Basic Path Configuration

Global Foundation

Configure the base structure that all uploads will use:

lib/upload.ts
import { createUploadConfig } from "pushduck/server";

const { s3 } = createUploadConfig()
  .provider("cloudflareR2",{
    // ... provider config
  })
  .paths({
    // Global prefix - foundation for ALL uploads
    prefix: "uploads",
    
    // Global structure - used when routes don't override
    generateKey: (file, metadata) => {
      const userId = metadata.userId || "anonymous";
      const timestamp = Date.now();
      const randomId = Math.random().toString(36).substring(2, 8);
      const sanitizedName = file.name.replace(/[^a-zA-Z0-9.-]/g, "_");

      // Return ONLY the file path part (no prefix)
      return `${userId}/${timestamp}/${randomId}/${sanitizedName}`;
    },
  })
  .build();

Route-Level Extensions

Extend the global foundation with route-specific organization:

app/api/upload/route.ts
import { s3 } from "@/lib/upload";

const s3Router = s3.createRouter({
  // Images: uploads/images/{userId}/{timestamp}/{randomId}/photo.jpg
  imageUpload: s3
    .image()
    .max("5MB")
    .paths({
      prefix: "images", // Nests under global prefix
    }),

  // Documents: uploads/documents/{userId}/{timestamp}/{randomId}/report.pdf  
  documentUpload: s3
    .file()
    .max("10MB")
    .paths({
      prefix: "documents", // Nests under global prefix
    }),

  // General: uploads/{userId}/{timestamp}/{randomId}/file.ext
  generalUpload: s3
    .file()
    .max("20MB")
    // No .paths() - uses pure global configuration
});

✨ Result: Clean, predictable paths that scale with your application. Global config provides consistency while routes add organization.

Advanced Path Patterns

Custom Route Generation

Override the default structure for specific use cases:

galleryUpload: s3
  .image()
  .max("5MB")
  .paths({
    generateKey: (ctx) => {
      const { file, metadata, globalConfig } = ctx;
      const globalPrefix = globalConfig.prefix || "uploads";
      const date = new Date();
      const year = date.getFullYear();
      const month = String(date.getMonth() + 1).padStart(2, "0");
      const userId = metadata.userId || "anonymous";
      
      // Custom path: uploads/gallery/2024/06/demo-user/photo.jpg
      return `${globalPrefix}/gallery/${year}/${month}/${userId}/${file.name}`;
    },
  })

Result: uploads/gallery/2024/06/demo-user/photo.jpg

productUpload: s3
  .image()
  .max("8MB")
  .paths({
    generateKey: (ctx) => {
      const { file, metadata, globalConfig } = ctx;
      const globalPrefix = globalConfig.prefix || "uploads";
      const category = metadata.category || "general";
      const productId = metadata.productId || "unknown";
      const timestamp = Date.now();
      
      // Custom path: uploads/products/electronics/prod-123/1234567890/image.jpg
      return `${globalPrefix}/products/${category}/${productId}/${timestamp}/${file.name}`;
    },
  })

Result: uploads/products/electronics/prod-123/1234567890/image.jpg

profileUpload: s3
  .image()
  .max("2MB")
  .paths({
    generateKey: (ctx) => {
      const { file, metadata, globalConfig } = ctx;
      const globalPrefix = globalConfig.prefix || "uploads";
      const userId = metadata.userId || "anonymous";
      const fileType = file.type.split('/')[0]; // image, video, etc.
      
      // Custom path: uploads/users/user-123/profile/image/avatar.jpg
      return `${globalPrefix}/users/${userId}/profile/${fileType}/${file.name}`;
    },
  })

Result: uploads/users/user-123/profile/image/avatar.jpg

Environment-Based Paths

Organize files by environment for clean separation:

lib/upload.ts
const { s3 } = createUploadConfig()
  .provider("cloudflareR2",{
    // ... provider config
  })
  .paths({
    // Environment-aware global prefix
    prefix: process.env.NODE_ENV === "production" ? "prod" : "dev",
    
    generateKey: (file, metadata) => {
      const userId = metadata.userId || "anonymous";
      const timestamp = Date.now();
      const randomId = Math.random().toString(36).substring(2, 8);
      
      return `${userId}/${timestamp}/${randomId}/${file.name}`;
    },
  })
  .build();

Result Paths:

  • Development: dev/images/user123/1234567890/abc123/photo.jpg
  • Production: prod/images/user123/1234567890/abc123/photo.jpg

Path Configuration API

Global Configuration

PropTypeDefault
generateKey?
(file, metadata) => string
-
prefix?
string
"uploads"
.paths({
  prefix: "uploads",
  generateKey: (file, metadata) => {
    // Return the file path structure (without prefix)
    return `${metadata.userId}/${Date.now()}/${file.name}`;
  }
})

Route Configuration

PropTypeDefault
generateKey?
(ctx: PathContext) => string
-
prefix?
string
-
.paths({
  prefix: "images", // Nested under global prefix
  generateKey: (ctx) => {
    const { file, metadata, globalConfig, routeName } = ctx;
    // Full control with access to global configuration
    return `${globalConfig.prefix}/custom/${file.name}`;
  }
})

PathContext Reference

When using custom generateKey functions at the route level, you receive a context object:

PropTypeDefault
routeName?
string
-
globalConfig?
{ prefix?: string; generateKey?: Function }
-
metadata?
TMetadata
-
file?
{ name: string; type: string }
-

Real-World Examples

E-commerce Platform

app/api/upload/route.ts
const s3Router = s3.createRouter({
  // Product images: uploads/products/{category}/{productId}/images/photo.jpg
  productImages: s3
    .image()
    .max("8MB")
    .formats(["jpeg", "png", "webp"])
    .middleware(async ({ req }) => {
      const { productId, category } = await getProductContext(req);
      return { productId, category, userId: await getUserId(req) };
    })
    .paths({
      generateKey: (ctx) => {
        const { file, metadata, globalConfig } = ctx;
        const { productId, category } = metadata;
        return `${globalConfig.prefix}/products/${category}/${productId}/images/${file.name}`;
      },
    }),

  // User avatars: uploads/users/{userId}/avatar/profile.jpg
  userAvatar: s3
    .image()
    .max("2MB")
    .formats(["jpeg", "png", "webp"])
    .middleware(async ({ req }) => {
      const userId = await getUserId(req);
      return { userId, type: "avatar" };
    })
    .paths({
      generateKey: (ctx) => {
        const { file, metadata, globalConfig } = ctx;
        return `${globalConfig.prefix}/users/${metadata.userId}/avatar/${file.name}`;
      },
    }),

  // Order documents: uploads/orders/{orderId}/documents/{timestamp}/receipt.pdf
  orderDocuments: s3
    .file()
    .max("10MB")
    .types(["application/pdf", "image/*"])
    .middleware(async ({ req }) => {
      const { orderId } = await getOrderContext(req);
      return { orderId, uploadedAt: new Date().toISOString() };
    })
    .paths({
      generateKey: (ctx) => {
        const { file, metadata, globalConfig } = ctx;
        const timestamp = Date.now();
        return `${globalConfig.prefix}/orders/${metadata.orderId}/documents/${timestamp}/${file.name}`;
      },
    }),
});

Content Management System

app/api/upload/route.ts
const s3Router = s3.createRouter({
  // Media library: uploads/media/{year}/{month}/{type}/filename.ext
  mediaLibrary: s3
    .file()
    .max("50MB")
    .middleware(async ({ req, file }) => {
      const userId = await getUserId(req);
      const mediaType = file.type.split('/')[0]; // image, video, audio
      return { userId, mediaType };
    })
    .paths({
      generateKey: (ctx) => {
        const { file, metadata, globalConfig } = ctx;
        const date = new Date();
        const year = date.getFullYear();
        const month = String(date.getMonth() + 1).padStart(2, "0");
        const randomId = Math.random().toString(36).substring(2, 8);
        
        return `${globalConfig.prefix}/media/${year}/${month}/${metadata.mediaType}/${randomId}-${file.name}`;
      },
    }),

  // Page assets: uploads/pages/{pageSlug}/assets/image.jpg
  pageAssets: s3
    .image()
    .max("10MB")
    .middleware(async ({ req }) => {
      const { pageSlug } = await getPageContext(req);
      return { pageSlug, userId: await getUserId(req) };
    })
    .paths({
      generateKey: (ctx) => {
        const { file, metadata, globalConfig } = ctx;
        return `${globalConfig.prefix}/pages/${metadata.pageSlug}/assets/${file.name}`;
      },
    }),

  // Temp uploads: uploads/temp/{userId}/{sessionId}/file.ext
  tempUploads: s3
    .file()
    .max("20MB")
    .middleware(async ({ req }) => {
      const userId = await getUserId(req);
      const sessionId = req.headers.get('x-session-id') || 'unknown';
      return { userId, sessionId, temporary: true };
    })
    .paths({
      prefix: "temp", // Simple prefix approach
    }),
});

Best Practices

🎯 Path Organization

Use Descriptive Prefixes

Make paths self-documenting

// ✅ Good
.paths({ prefix: "user-avatars" })
.paths({ prefix: "product-images" })

// ❌ Avoid
.paths({ prefix: "imgs" })
.paths({ prefix: "files" })

Include Identifiers

Make files traceable

// ✅ Good - includes user and timestamp
return `${prefix}/users/${userId}/${timestamp}/${file.name}`;

// ❌ Avoid - no traceability
return `${prefix}/${file.name}`;

Prevent Conflicts

Use unique identifiers

// ✅ Good - timestamp + random ID
const timestamp = Date.now();
const randomId = Math.random().toString(36).substring(2, 8);
return `${prefix}/${userId}/${timestamp}/${randomId}/${file.name}`;

🚀 Performance Tips

Path Length Limits: Most S3-compatible services have a 1024-character limit for object keys. Keep your paths reasonable!

  • Use short, meaningful prefixes instead of long descriptive names
  • Avoid deep nesting beyond 5-6 levels for better performance
  • Include timestamps for natural chronological ordering
  • Sanitize filenames to prevent special character issues

🔒 Security Considerations

// ✅ Sanitize user input in paths
const sanitizePathSegment = (input: string) => {
  return input.replace(/[^a-zA-Z0-9.-]/g, "_").substring(0, 50);
};

.paths({
  generateKey: (ctx) => {
    const { file, metadata, globalConfig } = ctx;
    const safeUserId = sanitizePathSegment(metadata.userId);
    const safeFilename = sanitizePathSegment(file.name);
    
    return `${globalConfig.prefix}/users/${safeUserId}/${Date.now()}/${safeFilename}`;
  }
})

Migration from Legacy Paths

Upgrading from v1.x? The old pathPrefix option still works but is deprecated. Use the new hierarchical system for better organization.

Before (Legacy)

// Old way - simple prefix only
export const { POST, GET } = s3Router.handlers;

After (Hierarchical)

// New way - hierarchical configuration
const { s3 } = createUploadConfig()
  .provider("cloudflareR2",{ /* config */ })
  .paths({
    prefix: "uploads",
    generateKey: (file, metadata) => {
      return `${metadata.userId}/${Date.now()}/${file.name}`;
    }
  })
  .build();

const s3Router = s3.createRouter({
  images: s3.image().paths({ prefix: "images" }),
  documents: s3.file().paths({ prefix: "documents" })
});

Benefits of upgrading:

  • Better organization with route-specific prefixes
  • No configuration loss - global settings are preserved
  • More flexibility with custom generation functions
  • Type safety with PathContext
  • Scalable patterns for growing applications

🎉 You're ready! Your uploads now have clean, organized, and scalable path structures that will grow with your application.