Nitro/H3
Universal web server file uploads with Nitro and H3 using Web Standards - no adapter needed!
Nitro/H3 Integration
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 seamless with zero overhead and universal deployment capabilities.
Quick Setup
Install dependencies
npm install pushduck
yarn add pushduck
pnpm add pushduck
bun add pushduck
Configure 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().max("5MB"),
documentUpload: s3.file().max("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.handlers(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.handlers(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.handlers(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()
.max("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()
.max("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.handlers(event.node.req);
}));
// Health check
app.use('/health', defineEventHandler(() => ({ status: 'ok' })));
export default toNodeListener(app);
Client-Side Usage
HTML with Vanilla JavaScript
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>File Upload Demo</title>
<script src="https://unpkg.com/pushduck@latest/client.js"></script>
</head>
<body>
<div class="container">
<h1>File Upload Demo</h1>
<div id="upload-area">
<div class="upload-section">
<h3>Image Upload</h3>
<div id="image-upload"></div>
</div>
<div class="upload-section">
<h3>Document Upload</h3>
<div id="document-upload"></div>
</div>
</div>
</div>
<script>
const { useUpload } = pushduck;
const { UploadButton, UploadDropzone } = useUpload({
endpoint: "/api/upload",
});
// Mount upload components
const imageUpload = new UploadButton({
endpoint: 'imageUpload',
onClientUploadComplete: (files) => {
console.log('Files uploaded:', files);
alert('Upload completed!');
},
onUploadError: (error) => {
console.error('Upload error:', error);
alert(`Upload failed: ${error.message}`);
}
});
const documentUpload = new UploadDropzone({
endpoint: 'documentUpload',
onClientUploadComplete: (files) => {
console.log('Files uploaded:', files);
alert('Upload completed!');
},
onUploadError: (error) => {
console.error('Upload error:', error);
alert(`Upload failed: ${error.message}`);
}
});
imageUpload.mount('#image-upload');
documentUpload.mount('#document-upload');
</script>
</body>
</html>
With Framework Integration
import { useUpload } from "pushduck/client";
import type { AppUploadRouter } from "~/lib/upload";
export const { UploadButton, UploadDropzone } = useUpload<AppUploadRouter>({
endpoint: "/api/upload",
});
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>
<script src="https://unpkg.com/pushduck@latest/client.js"></script>
</head>
<body>
<div class="container">
<h1>My Files</h1>
<div id="upload-area">
<div id="file-upload"></div>
</div>
<div id="files-list">
<h2>Uploaded Files</h2>
<div id="files-grid"></div>
</div>
</div>
<script>
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 upload component
const { UploadDropzone } = pushduck.useUpload({
endpoint: "/api/upload",
});
const fileUpload = new UploadDropzone({
endpoint: 'imageUpload',
onClientUploadComplete: (files) => {
console.log('Files uploaded:', files);
loadFiles(); // Refresh file list
},
onUploadError: (error) => {
console.error('Upload error:', error);
alert(`Upload failed: ${error.message}`);
}
});
fileUpload.mount('#file-upload');
// 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.0
Performance 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.