API Reference/Configuration

Server Router Configuration

Complete guide to configuring your upload router with type safety and advanced features

Server Router Configuration

The server router is the heart of pushduck. It defines your upload endpoints with complete type safety and validates all uploads against your schema.

This guide covers the enhanced router API with property-based client access. Looking for the legacy API? Check our migration guide.

Project Structure

A typical Next.js project with pushduck follows this structure:

.env.local
next.config.js

Basic Router Setup

Create your upload route

Start by creating the API route that will handle your uploads:

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

const s3Router = s3.createRouter({
  imageUpload: s3
    .image()
    .max('4MB')
    .count(10)
    .middleware(async ({ req }) => {
    // Add your authentication logic here
    return { userId: "user_123" }
  }),

  documentUpload: s3
    .file()
    .max('10MB')
    .types(['application/pdf'])
    .count(1)
})

export const { POST, GET } = s3Router.handlers

Export router types

Create a separate file to export your router types for client-side usage:

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

// Configure upload with provider and settings
const { s3, storage } = createUploadConfig()
  .provider("cloudflareR2",{
    accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
    secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
    region: 'auto',
    endpoint: process.env.AWS_ENDPOINT_URL!,
    bucket: process.env.S3_BUCKET_NAME!,
    accountId: process.env.R2_ACCOUNT_ID!,
  })
  .build()

// Define your router
const s3Router = s3.createRouter({
  imageUpload: s3.image().max('4MB').count(10),
  documentUpload: s3.file().max('10MB').types(['application/pdf']).count(1)
})

// Export for use in API routes and client
export { s3, storage }
export type AppS3Router = typeof s3Router

Create typed client

Set up your client-side upload client with full type safety:

lib/upload-client.ts
import { createUploadClient } from 'pushduck/client'
import type { AppS3Router } from './upload'

export const upload = createUploadClient<AppS3Router>({
  endpoint: '/api/upload'
})

Schema Builder Reference

The s3 schema builders provide a fluent API for defining your upload requirements:

PropTypeDefault
any?
FileConfig
-
pdf?
FileConfig
-
video?
FileConfig
-
image?
FileConfig
-

FileConfig Options

PropTypeDefault
processing?
ProcessingConfig
-
mimeTypes?
string[]
-
maxCount?
number
1
maxSize?
string
"1MB"

Advanced Configuration

Multiple File Types

You can define schemas that accept multiple file types:

const s3Router = s3.createRouter({
  mixedUpload: s3.object({
    images: s3.image().max('4MB').count(5),
    pdfs: s3.file().max('10MB').types(['application/pdf']).count(2),
    documents: s3.file().max('5MB').types(['application/vnd.openxmlformats-officedocument.wordprocessingml.document']).count(3)
  })
})
const s3Router = s3.createRouter({
  mediaUpload: s3.object({
    images: s3.image()
      .max('4MB')
      .count(10)
      .formats(['jpeg', 'jpg', 'png', 'webp']),
    videos: s3.file()
      .max('100MB')
      .count(2)
      .types(['video/mp4', 'video/quicktime', 'video/avi'])
  })
})
const s3Router = s3.createRouter({
  genericUpload: s3.file()
    .max('50MB')
    .count(20)
    .types([
      'image/*', 'video/*', 'application/pdf',
      'application/msword', 'text/plain'
    ])
})

Global Configuration

Configure settings that apply to all upload endpoints:

PropTypeDefault
pathPrefix?
string
-
uploadTimeout?
number
300000
allowedOrigins?
string[]
["*"]
maxTotalSize?
string
"100MB"

Deprecated: The pathPrefix option is deprecated. Use the new hierarchical path configuration for better organization and flexibility.

// ❌ Deprecated approach - use modern createUploadConfig() instead
// This section is kept for reference only

For new projects, use the hierarchical path system instead:

// ✅ Modern approach with hierarchical paths
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" })
});

Multiple Providers

Support different storage providers for different upload types:

// ✅ Modern approach with multiple provider configurations
import { createUploadConfig } from "pushduck/server";

const primaryConfig = createUploadConfig()
  .provider("cloudflareR2",{
    // Primary R2 config for production files
    accessKeyId: process.env.R2_ACCESS_KEY_ID!,
    secretAccessKey: process.env.R2_SECRET_ACCESS_KEY!,
    accountId: process.env.R2_ACCOUNT_ID!,
    bucket: process.env.R2_BUCKET!,
  })
  .build();

const backupConfig = createUploadConfig()
  .provider("aws",{
    // AWS S3 config for backups
    accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
    secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
    region: process.env.AWS_REGION!,
    bucket: process.env.AWS_BACKUP_BUCKET!,
  })
  .build();

// Use primary config for main uploads
const s3Router = primaryConfig.s3.createRouter({
  productImages: primaryConfig.s3.image().max("4MB").count(10),
  // Backup handling would be done in lifecycle hooks
});

Middleware Integration

Add authentication, logging, and custom validation:

Authentication Middleware

const s3Router = s3.createRouter({
  privateUploads: s3.image()
    .max("4MB")
    .count(5)
    .middleware(async ({ req, metadata }) => {
      const session = await getServerSession(req);

      if (!session?.user?.id) {
        throw new Error("Unauthorized");
      }

      return {
        ...metadata,
        userId: session.user.id,
        userRole: session.user.role,
      };
    }),

  publicUploads: s3.image()
    .max("1MB")
    .count(1), // No middleware = publicly accessible
});

File Validation Middleware

import { z } from "zod";

const s3Router = s3.createRouter({
  profilePicture: s3.image()
    .max("2MB")
    .count(1)
    .middleware(async ({ req, file, metadata }) => {
      // Custom file validation
      if (file.name.includes("temp") || file.name.includes("test")) {
        throw new Error("Temporary files not allowed");
      }

      const userId = await getUserId(req);
      return { ...metadata, userId };
    }),
});

Metadata Enhancement

const s3Router = s3.createRouter({
  documentUpload: s3.file()
    .max("10MB")
    .count(1)
    .types(["application/pdf"])
    .middleware(async ({ req, metadata }) => {
      const enrichedMetadata = {
        ...metadata,
        uploadedAt: new Date().toISOString(),
        userAgent: req.headers.get("user-agent"),
        ip: req.headers.get("x-forwarded-for") || "unknown",
      };

      return enrichedMetadata;
    }),
});

Lifecycle Hooks

Handle upload events for processing, notifications, and cleanup:

PropTypeDefault
onUploadError?
(context, error) => void | Promise<void>
-
onUploadComplete?
(context, result) => void | Promise<void>
-
onUploadProgress?
(context, progress) => void | Promise<void>
-
onUploadStart?
(context) => void | Promise<void>
-
const s3Router = s3.createRouter({
  imageUpload: s3.image()
    .max("4MB")
    .count(10)
    .onUploadComplete(async ({ file, url, metadata }) => {
      // Process uploaded image
      await generateThumbnail(url);
      await updateDatabase(file.key, metadata.userId);
    })
    .onUploadError(async ({ error, metadata }) => {
      // Log errors and notify admins
      console.error("Upload failed:", error);
      await notifyAdmins(`Upload failed for user ${metadata.userId}`);
    }),
});

Type Safety Features

Router Type Export

Export your router type for end-to-end type safety:

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

const { s3 } = createUploadConfig()
  .provider("cloudflareR2",{ /* your config */ })
  .build();

const s3Router = s3.createRouter({
  // ... your configuration
});

export { s3 };
export type AppS3Router = typeof s3Router;

// Extract individual endpoint types
export type ImageUploadType = AppS3Router["imageUpload"];
export type DocumentUploadType = AppS3Router["documentUpload"];

Custom Context Types

Define custom context types for your middleware:

interface CustomContext {
  userId: string;
  userRole: "admin" | "user" | "guest";
  organizationId?: string;
}

const s3Router = s3.createRouter({
  upload: s3.image()
    .max('4MB')
    .count(5)
    .middleware(async ({ req }): Promise<CustomContext> => {
    // Your auth logic here
    return {
      userId: "user_123",
      userRole: "user",
    };
  }),
});

Real-World Examples

E-commerce Product Images

const ecommerceRouter = s3.createRouter({
  productImages: s3.image()
    .max('5MB')
    .count(8)
    .formats(['webp', 'jpeg'])
    .middleware(async ({ req }) => {
      const vendorId = await getVendorId(req);
      return { vendorId, category: "products" };
    })
    .onUploadComplete(async ({ files, metadata }) => {
      // Update product catalog
      await updateProductImages(metadata.vendorId, files);
    }),
});

Document Management System

const docsRouter = s3.createRouter({
  contracts: s3.file()
    .max('25MB')
    .types(['application/pdf'])
    .count(1)
    .middleware(async ({ req }) => {
    const { userId, companyId } = await validateContractUpload(req);
    return { userId, companyId, confidential: true };
  }),

  proposals: s3.object({
    pdfs: s3.file().max('50MB').types(['application/pdf']).count(3),
    documents: s3.file().max('25MB').types(['application/vnd.openxmlformats-officedocument.wordprocessingml.document']).count(5),
  }).middleware(async ({ req }) => {
    const { userId, projectId } = await validateProposalUpload(req);
    return { userId, projectId };
  }),
});

Social Media Platform

export const socialRouter = createUploadRouter({
  profilePicture: uploadSchema({
    image: {
      maxSize: "2MB",
      maxCount: 1,
      processing: {
        resize: { width: 400, height: 400 },
        format: "webp",
      },
    },
  }),

  postMedia: uploadSchema({
    image: { maxSize: "8MB", maxCount: 4 },
    video: { maxSize: "100MB", maxCount: 1 },
  }).middleware(async ({ req }) => {
    const userId = await authenticateUser(req);
    return { userId, postType: "media" };
  }),
});

Security Best Practices

Important: Always implement proper authentication and file validation in production environments.

Content Type Validation

export const router = createUploadRouter({
  secureUpload: uploadSchema({
    image: {
      maxSize: "4MB",
      maxCount: 5,
      mimeTypes: ["image/jpeg", "image/png", "image/webp"], // Explicit whitelist
    },
  }).middleware(async ({ req, files }) => {
    // Additional security checks
    for (const file of files) {
      // Validate file headers match content type
      const isValidImage = await validateImageFile(file);
      if (!isValidImage) {
        throw new Error("Invalid image file");
      }
    }

    return { userId: await getUserId(req) };
  }),
});

Rate Limiting

import { ratelimit } from "@/lib/ratelimit";

export const router = createUploadRouter({
  upload: uploadSchema({
    any: { maxSize: "10MB", maxCount: 3 },
  }).middleware(async ({ req }) => {
    const ip = req.headers.get("x-forwarded-for") || "unknown";

    const { success } = await ratelimit.limit(ip);
    if (!success) {
      throw new Error("Rate limit exceeded");
    }

    return { ip };
  }),
});

Next Steps: Now that you have your router configured, learn how to configure your client for the best developer experience.