Fresh
Deno-powered file uploads with Fresh using Web Standards - no adapter needed!
🚧 Client-Side In Development: Fresh server-side integration is fully functional with Web Standards APIs. However, Fresh-specific client-side components and hooks are still in development. You can use the standard pushduck client APIs for now.
Using pushduck with Fresh
Fresh is a modern web framework for Deno that uses islands architecture for optimal performance. It uses Web Standards APIs and provides server-side rendering with minimal client-side JavaScript. Since Fresh uses standard Request/Response objects, pushduck handlers work directly without any adapters!
Web Standards Native: Fresh API routes use Web Standard Request/Response objects, making pushduck integration straightforward with zero overhead.
Quick Setup
Install Fresh and pushduck
# Create a new Fresh project
deno run -A -r https://fresh.deno.dev my-app
cd my-app
# Add pushduck to import_map.json{
"imports": {
"$fresh/": "https://deno.land/x/fresh@1.6.1/",
"preact": "https://esm.sh/preact@10.19.2",
"preact/": "https://esm.sh/preact@10.19.2/",
"pushduck/server": "https://esm.sh/pushduck@latest/server",
"pushduck/core": "https://esm.sh/pushduck@latest/core"
}
}# Create a new Fresh project
deno run -A -r https://fresh.deno.dev my-app
cd my-app
# Install pushduck via npm (requires Node.js compatibility)
npm install pushduck# Create a new Fresh project
deno run -A -r https://fresh.deno.dev my-app
cd my-app
# Install pushduck via yarn (requires Node.js compatibility)
yarn add pushduck# Create a new Fresh project
deno run -A -r https://fresh.deno.dev my-app
cd my-app
# Install pushduck via pnpm (requires Node.js compatibility)
pnpm add pushduck# Create a new Fresh project
deno run -A -r https://fresh.deno.dev my-app
cd my-app
# Install pushduck via bun (requires Node.js compatibility)
bun add pushduckConfigure upload router
import { createUploadConfig } from 'pushduck/server';
const { s3, createS3Router } = createUploadConfig()
.provider("cloudflareR2",{
accessKeyId: Deno.env.get("AWS_ACCESS_KEY_ID")!,
secretAccessKey: Deno.env.get("AWS_SECRET_ACCESS_KEY")!,
region: 'auto',
endpoint: Deno.env.get("AWS_ENDPOINT_URL")!,
bucket: Deno.env.get("S3_BUCKET_NAME")!,
accountId: Deno.env.get("R2_ACCOUNT_ID")!,
})
.build();
export const uploadRouter = createS3Router({
imageUpload: s3.image().maxFileSize("5MB"),
documentUpload: s3.file().maxFileSize("10MB")
});
export type AppUploadRouter = typeof uploadRouter;Create API route
import { Handlers } from "$fresh/server.ts";
import { uploadRouter } from "../../../lib/upload.ts";
// Direct usage - no adapter needed!
export const handler: Handlers = {
async GET(req) {
return uploadRouter.handler(req);
},
async POST(req) {
return uploadRouter.handler(req);
},
};Basic Integration
Simple Upload Route
import { Handlers } from "$fresh/server.ts";
import { uploadRouter } from "../../../lib/upload.ts";
// Method 1: Combined handler (recommended)
export const handler: Handlers = {
async GET(req) {
return uploadRouter.handler(req);
},
async POST(req) {
return uploadRouter.handler(req);
},
};
// Method 2: Universal handler
export const handler: Handlers = {
async GET(req) {
return uploadRouter.handler(req);
},
async POST(req) {
return uploadRouter.handler(req);
},
async OPTIONS(req) {
return new Response(null, {
status: 200,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
},
});
},
};With Middleware
import { MiddlewareHandlerContext } from "$fresh/server.ts";
export async function handler(
req: Request,
ctx: MiddlewareHandlerContext,
) {
// Add CORS headers for upload routes
if (ctx.destination === "route" && req.url.includes("/api/upload")) {
const response = await ctx.next();
response.headers.set("Access-Control-Allow-Origin", "*");
response.headers.set("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
response.headers.set("Access-Control-Allow-Headers", "Content-Type");
return response;
}
return ctx.next();
}Advanced Configuration
Authentication with Fresh
import { createUploadConfig } from 'pushduck/server';
import { getCookies } from "https://deno.land/std@0.208.0/http/cookie.ts";
const { s3, createS3Router } = createUploadConfig()
.provider("cloudflareR2",{
accessKeyId: Deno.env.get("AWS_ACCESS_KEY_ID")!,
secretAccessKey: Deno.env.get("AWS_SECRET_ACCESS_KEY")!,
region: 'auto',
endpoint: Deno.env.get("AWS_ENDPOINT_URL")!,
bucket: Deno.env.get("S3_BUCKET_NAME")!,
accountId: Deno.env.get("R2_ACCOUNT_ID")!,
})
.paths({
prefix: 'uploads',
generateKey: (file, metadata) => {
return `${metadata.userId}/${Date.now()}/${file.name}`;
}
})
.build();
export const uploadRouter = createS3Router({
// Private uploads with cookie-based authentication
privateUpload: s3
.image()
.maxFileSize("5MB")
.middleware(async ({ req }) => {
const cookies = getCookies(req.headers);
const sessionId = cookies.sessionId;
if (!sessionId) {
throw new Error('Authentication required');
}
const user = await getUserFromSession(sessionId);
if (!user) {
throw new Error('Invalid session');
}
return {
userId: user.id,
username: user.username,
};
}),
// Public uploads (no auth)
publicUpload: s3
.image()
.maxFileSize("2MB")
// No middleware = public access
});
export type AppUploadRouter = typeof uploadRouter;
// Helper function
async function getUserFromSession(sessionId: string) {
// Implement your session validation logic
// This could connect to a database, Deno KV, etc.
return { id: 'user-123', username: 'demo-user' };
}Client-Side Usage
Fresh islands run Preact in the browser. pushduck ships no Preact-specific
binding, and does not need one — pushduck/core exposes the upload engine
directly and wires into Preact hooks in a few lines.
pushduck/core has no framework dependency and no server code. It is the
same engine the React, Vue, Svelte, and Solid bindings wrap, so upload
behaviour is identical everywhere.
The Engine API
import { createUploadEngine } from "pushduck/core";
const engine = createUploadEngine({
endpoint: "/api/upload",
route: "imageUpload",
});
engine.subscribe(() => {
const { files, progress, isUploading, errors } = engine.getSnapshot();
// render however your framework prefers
});
await engine.upload([...fileInput.files]);| Member | Purpose |
|---|---|
upload(files, metadata?) | Start a batch; resolves when it settles |
getSnapshot() | Current state — stable reference between changes |
subscribe(listener) | Register a change listener; returns unsubscribe |
cancel(fileId) | Abort one in-flight file |
cancelAll() | Abort every in-flight file |
reset() | Abort everything and clear state |
Upload Island
import { useEffect, useRef, useState } from "preact/hooks";
import {
createUploadEngine,
type UploadEngine,
type UploadEngineState,
} from "pushduck/core";
export default function FileUpload() {
const engineRef = useRef<UploadEngine | null>(null);
const [state, setState] = useState<UploadEngineState | null>(null);
useEffect(() => {
const engine = createUploadEngine({
endpoint: "/api/upload",
route: "imageUpload",
onError: (error) => console.error("Upload failed:", error),
});
engineRef.current = engine;
setState(engine.getSnapshot());
// subscribe() returns its own unsubscribe — hand it straight back.
return engine.subscribe(() => setState(engine.getSnapshot()));
}, []);
if (!state) return <p>Loading…</p>;
return (
<div class="space-y-4">
<input
type="file"
multiple
accept="image/*"
disabled={state.isUploading}
onChange={(e) =>
engineRef.current?.upload([
...((e.target as HTMLInputElement).files ?? []),
])
}
/>
{state.isUploading && <progress value={state.progress} max={100} />}
{state.files.map((file) => (
<div key={file.id} class="flex gap-2 items-center">
<span>{file.name}</span>
<span>
{file.status} ({file.progress}%)
</span>
{file.status === "uploading" && (
<button onClick={() => engineRef.current?.cancel(file.id)}>
Cancel
</button>
)}
</div>
))}
</div>
);
}Passing Client Metadata
await engine.upload(files, { albumId: "vacation-2026", visibility: "private" });Client metadata is untrusted user input. Validate and sanitize it in your
server middleware, and never trust client-provided identity claims such as
userId or role — derive those from the authenticated session instead.
File Management
Server-Side File API
import { Handlers } from "$fresh/server.ts";
export const handler: Handlers = {
async GET(req) {
const url = new URL(req.url);
const userId = url.searchParams.get('userId');
if (!userId) {
return new Response(JSON.stringify({ error: 'User ID required' }), {
status: 400,
headers: { 'Content-Type': 'application/json' }
});
}
// Fetch files from database/Deno KV
const files = await getFilesForUser(userId);
return new Response(JSON.stringify({
files: files.map(file => ({
id: file.id,
name: file.name,
url: file.url,
size: file.size,
uploadedAt: file.createdAt,
})),
}), {
headers: { 'Content-Type': 'application/json' }
});
},
};
async function getFilesForUser(userId: string) {
// Example using Deno KV
const kv = await Deno.openKv();
const files = [];
for await (const entry of kv.list({ prefix: ["files", userId] })) {
files.push(entry.value);
}
return files;
}File Management Page
import { Head } from "$fresh/runtime.ts";
import { Handlers, PageProps } from "$fresh/server.ts";
import FileUpload from "../islands/FileUpload.tsx";
interface FileData {
id: string;
name: string;
url: string;
size: number;
uploadedAt: string;
}
interface PageData {
files: FileData[];
}
export const handler: Handlers<PageData> = {
async GET(req, ctx) {
// Fetch files for current user
const files = await getFilesForUser("current-user");
return ctx.render({ files });
},
};
export default function FilesPage({ data }: PageProps<PageData>) {
function formatFileSize(bytes: number): string {
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
if (bytes === 0) return '0 Bytes';
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
}
return (
<>
<Head>
<title>My Files</title>
</Head>
<main class="container mx-auto px-4 py-8">
<h1 class="text-3xl font-bold mb-8">My Files</h1>
<div class="mb-8">
<FileUpload />
</div>
<div>
<h2 class="text-2xl font-semibold mb-4">Uploaded Files</h2>
{data.files.length === 0 ? (
<p class="text-gray-500">No files uploaded yet.</p>
) : (
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{data.files.map((file) => (
<div key={file.id} class="border rounded-lg p-4 hover:shadow-md transition-shadow">
<h3 class="font-medium truncate" title={file.name}>
{file.name}
</h3>
<p class="text-sm text-gray-500">
{formatFileSize(file.size)}
</p>
<p class="text-sm text-gray-500">
{new Date(file.uploadedAt).toLocaleDateString()}
</p>
<a
href={file.url}
target="_blank"
rel="noopener noreferrer"
class="text-blue-500 hover:underline text-sm mt-2 inline-block"
>
View File
</a>
</div>
))}
</div>
)}
</div>
</main>
</>
);
}
async function getFilesForUser(userId: string) {
// Implementation depends on your storage solution
return [];
}Deployment Options
# Deploy to Deno Deploy
deno task build
deployctl deploy --project=my-app --include=. --exclude=node_modules{
"tasks": {
"build": "deno run -A dev.ts build",
"preview": "deno run -A main.ts",
"start": "deno run -A --watch=static/,routes/ dev.ts",
"deploy": "deployctl deploy --project=my-app --include=. --exclude=node_modules"
}
}FROM denoland/deno:1.38.0
WORKDIR /app
# Copy dependency files
COPY deno.json deno.lock import_map.json ./
# Cache dependencies
RUN deno cache --import-map=import_map.json main.ts
# Copy source code
COPY . .
# Build the application
RUN deno task build
EXPOSE 8000
CMD ["deno", "run", "-A", "main.ts"]# Install Deno
curl -fsSL https://deno.land/install.sh | sh
# Clone and run your app
git clone <your-repo>
cd <your-app>
deno task start[Unit]
Description=Fresh App
After=network.target
[Service]
Type=simple
User=deno
WorkingDirectory=/opt/fresh-app
ExecStart=/home/deno/.deno/bin/deno run -A main.ts
Restart=always
[Install]
WantedBy=multi-user.targetEnvironment Variables
# AWS Configuration
AWS_REGION=us-east-1
AWS_ACCESS_KEY_ID=your_access_key
AWS_SECRET_ACCESS_KEY=your_secret_key
AWS_S3_BUCKET=your-bucket-name
# Fresh
PORT=8000Performance Benefits
Islands Architecture
Only hydrate interactive components, minimal JavaScript
Web Standards
No adapter overhead - direct Request/Response usage
Deno Runtime
Modern runtime with built-in TypeScript and security
Edge Ready
Works on edge runtimes and CDNs
Real-Time Upload Progress
import { useState } from "preact/hooks";
export default function AdvancedUpload() {
const [uploadProgress, setUploadProgress] = useState(0);
const [isUploading, setIsUploading] = useState(false);
async function handleFileUpload(event: Event) {
const target = event.target as HTMLInputElement;
const files = target.files;
if (!files || files.length === 0) return;
setIsUploading(true);
setUploadProgress(0);
try {
// Simulate upload progress
for (let i = 0; i <= 100; i += 10) {
setUploadProgress(i);
await new Promise(resolve => setTimeout(resolve, 100));
}
alert('Upload completed!');
} catch (error) {
console.error('Upload failed:', error);
alert('Upload failed!');
} finally {
setIsUploading(false);
setUploadProgress(0);
}
}
return (
<div class="upload-container max-w-md mx-auto">
<input
type="file"
multiple
onChange={handleFileUpload}
disabled={isUploading}
class="w-full p-3 border-2 border-dashed border-gray-300 rounded-lg cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
/>
{isUploading && (
<div class="mt-4">
<div class="w-full h-2 bg-gray-200 rounded-full overflow-hidden">
<div
class="h-full bg-green-500 transition-all duration-300 ease-out"
style={{ width: `${uploadProgress}%` }}
/>
</div>
<p class="text-center mt-2 text-sm text-gray-600">
{uploadProgress}% uploaded
</p>
</div>
)}
</div>
);
}Deno KV Integration
// Example using Deno KV for file metadata storage
export class FileStorage {
private kv: Deno.Kv;
constructor() {
this.kv = await Deno.openKv();
}
async saveFileMetadata(userId: string, file: {
id: string;
name: string;
url: string;
size: number;
type: string;
}) {
const key = ["files", userId, file.id];
await this.kv.set(key, {
...file,
createdAt: new Date().toISOString(),
});
}
async getFilesForUser(userId: string) {
const files = [];
for await (const entry of this.kv.list({ prefix: ["files", userId] })) {
files.push(entry.value);
}
return files;
}
async deleteFile(userId: string, fileId: string) {
const key = ["files", userId, fileId];
await this.kv.delete(key);
}
}
export const fileStorage = new FileStorage();Troubleshooting
Common Issues
- Route not found: Ensure your route is
routes/api/upload/[...path].ts - Import errors: Check your
import_map.jsonconfiguration - Permissions: Deno requires explicit permissions (
-Aflag for all permissions) - Environment variables: Use
Deno.env.get()instead ofprocess.env
Debug Mode
Enable debug logging:
export const uploadRouter = createS3Router({
// ... routes
}).middleware(async ({ req, file }) => {
if (Deno.env.get("DENO_ENV") === "development") {
console.log("Upload request:", req.url);
console.log("File:", file.name, file.size);
}
return {};
});Fresh Configuration
import { defineConfig } from "$fresh/server.ts";
export default defineConfig({
plugins: [],
// Enable static file serving
staticDir: "./static",
// Custom build options
build: {
target: ["chrome99", "firefox99", "safari15"],
},
});Fresh provides an excellent foundation for building modern web applications with Deno and pushduck, combining the power of islands architecture with Web Standards APIs and Deno's secure runtime environment.