Framework Integrations
Universal file uploads that work with any web framework - from Web Standards to custom request/response APIs
Supported Frameworks
Pushduck provides universal file upload handlers that work with any web framework through a single, consistent API. Write your upload logic once and deploy it anywhere!
Universal Design: Pushduck uses Web Standards (Request/Response) at its core, making it compatible with both Web Standards frameworks and those with custom request/response APIs without framework-specific code.
Universal API
All frameworks use the same core API:
import { createS3Router, s3 } from 'pushduck/server';
const uploadRouter = createS3Router({
imageUpload: s3.image().maxFileSize("5MB"),
documentUpload: s3.file().maxFileSize("10MB"),
videoUpload: s3.file().maxFileSize("100MB").accept(["video/*"])
});
// Universal handlers - work with ANY framework
export const { GET, POST } = uploadRouter.handlers;Two Independent Questions
"Does pushduck support my framework?" is really two questions, and they have different answers. Read the table for your situation before adopting.
1. Can my backend mount the handler?
Web Standards Native
Frameworks using Web Request/Response
No adapter needed. Use uploadRouter.handler directly.
Hono · Elysia · Bun · Deno · Astro · SvelteKit · Nuxt/Nitro · Remix · TanStack Start · SolidStart · Fresh · Qwik · Cloudflare Workers · Vercel & Netlify edge
Custom Request/Response APIs
Frameworks with their own request objects
A small adapter is provided.
Next.js (App & Pages Router) · Express · Fastify
2. Which client does my frontend use?
Every binding wraps the same upload engine, so behaviour — progress, speed, ETA, cancellation, metadata, lifecycle hooks — is identical. Only the return shape changes, to match each framework's idiom.
| Frontend | Import | Entry point | State comes back as |
|---|---|---|---|
| React, Next.js | pushduck/client | useUploadRoute | plain values |
| React Native, Expo | pushduck/react-native | useUploadRoute | plain values |
| Vue, Nuxt | pushduck/vue | useUploadRoute | ComputedRefs |
| Svelte, SvelteKit | pushduck/svelte | createUpload | a readable store |
| SolidJS, SolidStart | pushduck/solid | createUpload | [state, actions] |
| Anything else | pushduck/core | createUploadEngine | subscribe / getSnapshot |
No binding for your framework? pushduck/core is the engine with no
framework attached — Preact, Qwik, Angular, Alpine, Lit, or plain JavaScript
wire into it via subscribe/getSnapshot in roughly fifty lines. The Qwik
and Fresh guides show exactly that.
A framework appearing in the server table above says nothing about the
client table. A Qwik app, for example, mounts the handler with no adapter,
but drives uploads through pushduck/core rather than a dedicated binding.
Quick Start by Framework
// Works with: Hono, Elysia, Bun, TanStack Start, SolidJS Start
import { uploadRouter } from '@/lib/upload';
// Direct usage - no adapter needed!
app.all('/api/upload/*', (ctx) => {
return uploadRouter.handler(ctx.request); // or c.req.raw
});// app/api/upload/route.ts
import { uploadRouter } from '@/lib/upload';
// Direct usage (recommended)
export const { GET, POST } = uploadRouter.handlers;
// Or with explicit adapter for extra type safety
import { toNextJsHandler } from 'pushduck/adapters';
export const { GET, POST } = toNextJsHandler(uploadRouter.handlers);import express from 'express';
import { uploadRouter } from '@/lib/upload';
import { toExpressHandler } from 'pushduck/adapters';
const app = express();
app.all("/api/upload/*", toExpressHandler(uploadRouter.handlers));import Fastify from 'fastify';
import { uploadRouter } from '@/lib/upload';
import { toFastifyHandler } from 'pushduck/adapters';
const fastify = Fastify();
fastify.all('/api/upload/*', toFastifyHandler(uploadRouter.handlers));Why Universal Handlers Work
Web Standards Foundation
Pushduck is built on Web Standards (Request and Response objects) that are supported by all modern JavaScript runtimes.
// Core handler signature
type Handler = (request: Request) => Promise<Response>Framework Compatibility
Modern frameworks expose Web Standard objects directly:
- Hono:
c.req.rawis a WebRequest - Elysia:
context.requestis a WebRequest - Bun: Native Web
Requestsupport - TanStack Start:
{ request }is a WebRequest - SolidJS Start:
event.requestis a WebRequest
Framework Adapters
For frameworks with custom request/response APIs, simple adapters convert between formats:
// Express adapter example
export function toExpressHandler(handlers: UniversalHandlers) {
return async (req: Request, res: Response, next: NextFunction) => {
const webRequest = convertExpressToWebRequest(req);
const webResponse = await handlers[req.method](webRequest);
convertWebResponseToExpress(webResponse, res);
};
}Configuration (Same for All Frameworks)
Your upload configuration is identical across all frameworks:
import { createUploadConfig } from 'pushduck/server';
const { s3, createS3Router } = 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!,
})
.paths({
prefix: 'uploads',
generateKey: (file, metadata) => {
return `${metadata.userId}/${Date.now()}/${file.name}`;
}
})
.build();
export const uploadRouter = createS3Router({
// Image uploads with validation
imageUpload: s3
.image()
.maxFileSize("5MB")
.accept(["image/jpeg", "image/png", "image/webp"])
.middleware(async ({ req }) => {
const userId = await getUserId(req);
return { userId, category: "images" };
}),
// Document uploads
documentUpload: s3
.file()
.maxFileSize("10MB")
.accept(["application/pdf", "text/plain"])
.middleware(async ({ req }) => {
const userId = await getUserId(req);
return { userId, category: "documents" };
}),
// Video uploads
videoUpload: s3
.file()
.maxFileSize("100MB")
.accept(["video/mp4", "video/quicktime"])
.middleware(async ({ req }) => {
const userId = await getUserId(req);
return { userId, category: "videos" };
})
});
export type AppUploadRouter = typeof uploadRouter;Client Usage (Framework Independent)
The client-side code is identical regardless of your backend framework:
import { createUploadClient } from 'pushduck/client';
import type { AppUploadRouter } from './upload';
export const upload = createUploadClient<AppUploadRouter>({
endpoint: '/api/upload'
});import { upload } from '@/lib/upload-client';
export function UploadForm() {
// Property-based access with full type safety
const { uploadFiles, files, isUploading } = upload.imageUpload();
const handleUpload = async (selectedFiles: File[]) => {
await uploadFiles(selectedFiles);
};
return (
<div>
<input
type="file"
multiple
onChange={(e) => handleUpload(Array.from(e.target.files || []))}
/>
{files.map(file => (
<div key={file.id}>
<span>{file.name}</span>
<progress value={file.progress} max={100} />
{file.url && <a href={file.url}>View</a>}
</div>
))}
</div>
);
}Benefits of Universal Design
Framework Flexibility
Switch frameworks without rewriting upload logic
Migrate from Express to Hono or Next.js to Bun without changing your upload implementation.
Performance
Zero overhead for modern frameworks
Web Standards native frameworks get direct handler access with no adapter overhead.
Developer Experience
One API to learn, works everywhere
Master pushduck once and use it with any framework in your toolkit.
Future Proof
Built on Web Standards
As more frameworks adopt Web Standards, they automatically work with pushduck.
Next Steps
Choose your framework integration guide:
Next.js
Complete guide for Next.js App Router and Pages Router
Hono
Fast, lightweight, built on Web Standards
Elysia
TypeScript-first framework with Bun
Express
Classic Node.js framework integration
Universal by Design: Write once, run anywhere. Pushduck's universal handlers make file uploads work across the entire JavaScript ecosystem.