Nitro/H3
Universal web server file uploads with Nitro and H3 using Web Standards - no adapter needed!
Using pushduck with Nitro/H3
Nitro is a universal web server framework that powers Nuxt.js, built on top of H3 (HTTP framework). It uses Web Standards APIs and provides excellent performance with universal deployment. Since Nitro/H3 uses standard Request/Response objects, pushduck handlers work directly without any adapters!
Web Standards Native: Nitro/H3 uses Web Standard Request/Response objects, making pushduck integration straightforward with zero overhead and universal deployment capabilities.
Quick Setup
Install dependencies
npm install pushduckyarn add pushduckpnpm add pushduckbun add pushduckConfigure upload router
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!,
})
.build();
export const uploadRouter = createS3Router({
imageUpload: s3.image().maxFileSize("5MB"),
documentUpload: s3.file().maxFileSize("10MB")
});
export type AppUploadRouter = typeof uploadRouter;Create API route
import { uploadRouter } from '~/lib/upload';
// Direct usage - no adapter needed!
export default defineEventHandler(async (event) => {
return uploadRouter.handler(event.node.req);
});Basic Integration
Simple Upload Route
import { uploadRouter } from '~/lib/upload';
// Method 1: Combined handler (recommended)
export default defineEventHandler(async (event) => {
return uploadRouter.handler(event.node.req);
});
// Method 2: Method-specific handlers
export default defineEventHandler(async (event) => {
const method = getMethod(event);
if (method === 'GET') {
return uploadRouter.handlers.GET(event.node.req);
}
if (method === 'POST') {
return uploadRouter.handlers.POST(event.node.req);
}
throw createError({
statusCode: 405,
statusMessage: 'Method Not Allowed'
});
});With H3 Utilities
import { uploadRouter } from '~/lib/upload';
import {
defineEventHandler,
getMethod,
setHeader,
createError
} from 'h3';
export default defineEventHandler(async (event) => {
// Handle CORS
setHeader(event, 'Access-Control-Allow-Origin', '*');
setHeader(event, 'Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
setHeader(event, 'Access-Control-Allow-Headers', 'Content-Type');
// Handle preflight requests
if (getMethod(event) === 'OPTIONS') {
return '';
}
try {
return await uploadRouter.handler(event.node.req);
} catch (error) {
throw createError({
statusCode: 500,
statusMessage: 'Upload failed',
data: error
});
}
});Advanced Configuration
Authentication with H3
import { createUploadConfig } from 'pushduck/server';
import { getCookie } from 'h3';
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({
// Private uploads with session authentication
privateUpload: s3
.image()
.maxFileSize("5MB")
.middleware(async ({ req }) => {
const cookies = req.headers.cookie;
const sessionId = parseCookie(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 functions
function parseCookie(cookieString: string | undefined) {
if (!cookieString) return {};
return Object.fromEntries(
cookieString.split('; ').map(c => {
const [key, ...v] = c.split('=');
return [key, v.join('=')];
})
);
}
async function getUserFromSession(sessionId: string) {
// Implement your session validation logic
return { id: 'user-123', username: 'demo-user' };
}Standalone Nitro App
Basic Nitro Setup
export default defineNitroConfig({
srcDir: 'server',
routeRules: {
'/api/upload/**': {
cors: true,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type'
}
}
},
experimental: {
wasm: true
}
});Server Entry Point
import { createApp, toNodeListener } from 'h3';
import { uploadRouter } from './lib/upload';
const app = createApp();
// Upload routes
app.use('/api/upload/**', defineEventHandler(async (event) => {
return uploadRouter.handler(event.node.req);
}));
// Health check
app.use('/health', defineEventHandler(() => ({ status: 'ok' })));
export default toNodeListener(app);Client-Side Usage
Nitro and H3 are server frameworks, so the client side is entirely your
choice. pushduck/core gives you the upload engine with no framework
attached — usable from plain JavaScript, a Web Component, or any
framework pushduck does not ship a binding for.
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 |
Vanilla Browser Usage
<input type="file" id="picker" multiple />
<progress id="bar" value="0" max="100"></progress>
<ul id="list"></ul>
<script type="module">
import { createUploadEngine } from "https://esm.sh/pushduck/core";
const engine = createUploadEngine({
endpoint: "/api/upload",
route: "imageUpload",
});
engine.subscribe(() => {
const { progress, files } = engine.getSnapshot();
document.querySelector("#bar").value = progress;
document.querySelector("#list").innerHTML = files
.map((f) => `<li>${f.name} — ${f.status} (${f.progress}%)</li>`)
.join("");
});
document.querySelector("#picker").addEventListener("change", (event) => {
engine.upload([...event.target.files]);
});
</script>Using a Framework Instead
Nitro is a server framework, so any frontend can sit in front of it. If yours is one pushduck ships a binding for, use that instead of the raw engine:
| Frontend | Import |
|---|---|
| React / Next.js | pushduck/client |
| Vue / Nuxt | pushduck/vue |
| Svelte / SvelteKit | pushduck/svelte |
| SolidJS | pushduck/solid |
| React Native / Expo | pushduck/react-native |
| Anything else | pushduck/core |
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
File API Route
import { defineEventHandler, getQuery, createError } from 'h3';
export default defineEventHandler(async (event) => {
const query = getQuery(event);
const userId = query.userId as string;
if (!userId) {
throw createError({
statusCode: 400,
statusMessage: 'User ID required'
});
}
// Fetch files from database
const files = await getFilesForUser(userId);
return {
files: files.map(file => ({
id: file.id,
name: file.name,
url: file.url,
size: file.size,
uploadedAt: file.createdAt,
})),
};
});
async function getFilesForUser(userId: string) {
// Implement your database query logic
return [];
}File Management Page
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My Files</title>
</head>
<body>
<div class="container">
<h1>My Files</h1>
<div id="upload-area">
<input type="file" id="file-input" multiple accept="image/*">
<progress id="upload-progress" value="0" max="100"></progress>
</div>
<div id="files-list">
<h2>Uploaded Files</h2>
<div id="files-grid"></div>
</div>
</div>
<script type="module">
import { createUploadEngine } from "https://esm.sh/pushduck/core";
async function loadFiles() {
try {
const response = await fetch('/api/files?userId=current-user');
const data = await response.json();
const filesGrid = document.getElementById('files-grid');
if (data.files.length === 0) {
filesGrid.innerHTML = '<p>No files uploaded yet.</p>';
return;
}
filesGrid.innerHTML = data.files.map(file => `
<div class="file-card">
<h3 title="${file.name}">${file.name}</h3>
<p>${formatFileSize(file.size)}</p>
<p>${new Date(file.uploadedAt).toLocaleDateString()}</p>
<a href="${file.url}" target="_blank" rel="noopener noreferrer">
View File
</a>
</div>
`).join('');
} catch (error) {
console.error('Failed to load files:', error);
}
}
function formatFileSize(bytes) {
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];
}
// Initialize the upload engine
const engine = createUploadEngine({
endpoint: '/api/upload',
route: 'imageUpload',
onSuccess: (files) => {
console.log('Files uploaded:', files);
loadFiles(); // Refresh file list
},
onError: (error) => {
console.error('Upload error:', error);
alert(`Upload failed: ${error.message}`);
}
});
engine.subscribe(() => {
const { progress, isUploading } = engine.getSnapshot();
document.querySelector('#upload-progress').value = progress;
document.querySelector('#file-input').disabled = isUploading;
});
document.querySelector('#file-input').addEventListener('change', (event) => {
engine.upload([...event.target.files]);
});
// Load files on page load
loadFiles();
</script>
</body>
</html>Deployment Options
export default defineNitroConfig({
preset: 'vercel-edge',
// or 'vercel' for Node.js runtime
});export default defineNitroConfig({
preset: 'netlify-edge',
// or 'netlify' for Node.js runtime
});export default defineNitroConfig({
preset: 'node-server',
});export default defineNitroConfig({
preset: 'cloudflare-workers',
});Environment 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
# Nitro
NITRO_PORT=3000
NITRO_HOST=0.0.0.0Performance Benefits
Universal Deployment
Deploy to any platform with unified API
Web Standards
No adapter overhead - direct Request/Response usage
Edge Ready
Works on edge runtimes and CDNs
Minimal Overhead
Lightweight framework with optimal performance
Middleware and Plugins
export default defineEventHandler(async (event) => {
if (event.node.req.url?.startsWith('/api/upload')) {
setHeader(event, 'Access-Control-Allow-Origin', '*');
setHeader(event, 'Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
setHeader(event, 'Access-Control-Allow-Headers', 'Content-Type');
if (getMethod(event) === 'OPTIONS') {
return '';
}
}
});export default async (nitroApp) => {
// Initialize database connection
console.log('Database plugin initialized');
// Add database to context
nitroApp.hooks.hook('request', async (event) => {
event.context.db = await getDatabase();
});
};Real-Time Upload Progress
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Advanced Upload</title>
</head>
<body>
<div class="upload-container">
<input type="file" id="fileInput" multiple />
<div id="progressContainer" style="display: none;">
<div id="progressBar"></div>
<p id="progressText">0% uploaded</p>
</div>
</div>
<script>
const fileInput = document.getElementById('fileInput');
const progressContainer = document.getElementById('progressContainer');
const progressBar = document.getElementById('progressBar');
const progressText = document.getElementById('progressText');
fileInput.addEventListener('change', async (event) => {
const files = event.target.files;
if (!files || files.length === 0) return;
progressContainer.style.display = 'block';
try {
// Simulate upload progress
for (let i = 0; i <= 100; i += 10) {
progressBar.style.width = `${i}%`;
progressText.textContent = `${i}% uploaded`;
await new Promise(resolve => setTimeout(resolve, 100));
}
alert('Upload completed!');
} catch (error) {
console.error('Upload failed:', error);
alert('Upload failed!');
} finally {
progressContainer.style.display = 'none';
progressBar.style.width = '0%';
}
});
</script>
<style>
.upload-container {
max-width: 400px;
margin: 0 auto;
padding: 20px;
}
#fileInput {
width: 100%;
padding: 12px;
border: 2px dashed #ccc;
border-radius: 8px;
cursor: pointer;
}
#progressContainer {
margin-top: 16px;
}
#progressBar {
height: 8px;
background-color: #4CAF50;
border-radius: 4px;
transition: width 0.3s ease;
width: 0%;
}
#progressText {
text-align: center;
margin-top: 8px;
font-size: 14px;
color: #666;
}
</style>
</body>
</html>Troubleshooting
Common Issues
- Route not found: Ensure your route is
routes/api/upload/[...path].ts - Build errors: Check that pushduck and h3 are properly installed
- CORS issues: Use Nitro's built-in CORS handling or middleware
- Environment variables: Make sure they're accessible in your deployment environment
Debug Mode
Enable debug logging:
export const uploadRouter = createS3Router({
// ... routes
}).middleware(async ({ req, file }) => {
if (process.env.NODE_ENV === "development") {
console.log("Upload request:", req.url);
console.log("File:", file.name, file.size);
}
return {};
});Nitro Configuration
export default defineNitroConfig({
srcDir: 'server',
buildDir: '.nitro',
output: {
dir: '.output',
serverDir: '.output/server',
publicDir: '.output/public'
},
runtimeConfig: {
awsAccessKeyId: process.env.AWS_ACCESS_KEY_ID,
awsSecretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
awsRegion: process.env.AWS_REGION,
s3BucketName: process.env.S3_BUCKET_NAME,
},
experimental: {
wasm: true
}
});Nitro/H3 provides an excellent foundation for building universal web applications with pushduck, offering flexibility, performance, and deployment options across any platform while maintaining full compatibility with Web Standards APIs.