Uploads
Published Oct 5, 2025 | Updated Mar 13, 2026
Add production-ready uploads with S3, R2, or Cloudinary
Overview
Add production-ready file upload flows (proxy, direct, multipart) to your backend.
Install
npx zuro add uploadsScaffolds upload routes/controllers/libs, injects route mounting, and writes provider-specific env variables.
What This Generates
src/
├ controllers/uploads.controller.ts
├ routes/uploads.routes.ts
├ middleware/upload-auth.ts
└ lib/uploads/
├ index.ts
├ types.ts
├ provider.ts
├ metadata.ts
└ proxy.ts
src/db/schema/uploads.ts # only when metadata storage is enabledcontrollers/uploads.controller.ts: input validation + HTTP handlers.routes/uploads.routes.ts: mode-specific endpoints under/api/uploads.middleware/upload-auth.ts: required/public upload access gate.lib/uploads/provider.ts: provider implementation (S3/R2/Cloudinary).lib/uploads/metadata.ts: metadata persistence or noop adapter.lib/uploads/proxy.ts:multermiddleware for proxy uploads.
Quick Example
Proxy mode request:
POST /api/uploads
Content-Type: multipart/form-data
file=<binary>curl -X POST http://localhost:3000/api/uploads \
-F "file=@./avatar.png"Example response:
{
"upload": {
"key": "uploads/public/2026/03/12/uuid-avatar.png",
"provider": "s3",
"access": "public",
"originalName": "avatar.png",
"mimeType": "image/png",
"bytes": 12345,
"url": "https://..."
},
"record": null
}How It Works
- Client calls an uploads endpoint.
- Route in
/api/uploads/*forwards toUploadsController. - Controller validates input and calls
lib/uploadsservice functions. - Service talks to provider client (S3, R2, or Cloudinary).
- Metadata is optionally stored.
- Standardized upload response is returned.
Configuration
UPLOAD_PROVIDER=s3
UPLOAD_MODE=direct
UPLOAD_AUTH_MODE=required
UPLOAD_FILE_ACCESS=private
UPLOAD_FILE_PRESET=image
UPLOAD_KEY_PREFIX=uploads
UPLOAD_ALLOWED_MIME=image/jpeg,image/png,image/webp,image/gif
UPLOAD_MAX_FILE_SIZE=5242880
UPLOAD_MAX_FILES=1
UPLOAD_DIRECT_URL_TTL_SECONDS=900
UPLOAD_ACCESS_URL_TTL_SECONDS=300
UPLOAD_MULTIPART_PART_SIZE=5242880
UPLOAD_BUCKET=my-bucket
UPLOAD_REGION=us-east-1
UPLOAD_ENDPOINT=
UPLOAD_ACCESS_KEY_ID=...
UPLOAD_SECRET_ACCESS_KEY=...
UPLOAD_PUBLIC_BASE_URL=UPLOAD_PROVIDER:s3,r2, orcloudinary.UPLOAD_MODE: upload flow (proxy,direct,large).UPLOAD_AUTH_MODE: who can upload (requiredornone).UPLOAD_FILE_ACCESS: resulting file access level (privateorpublic).UPLOAD_FILE_PRESET: preset defaults for allowed MIME and limits.UPLOAD_KEY_PREFIX: storage key prefix.UPLOAD_ALLOWED_MIME: comma-separated allowed MIME types.UPLOAD_MAX_FILE_SIZE: max bytes per file.UPLOAD_MAX_FILES: max files accepted by proxy middleware.UPLOAD_DIRECT_URL_TTL_SECONDS: expiry for direct upload signed URLs.UPLOAD_ACCESS_URL_TTL_SECONDS: expiry for private access URLs.UPLOAD_MULTIPART_PART_SIZE: suggested part size for multipart uploads.UPLOAD_BUCKET: S3/R2 bucket name.UPLOAD_REGION: S3 region orautofor R2.UPLOAD_ENDPOINT: custom endpoint (used for R2/S3-compatible storage).UPLOAD_ACCESS_KEY_ID: provider access key.UPLOAD_SECRET_ACCESS_KEY: provider secret key.UPLOAD_PUBLIC_BASE_URL: optional CDN/public base URL.
Cloudinary keys:
CLOUDINARY_CLOUD_NAME=...
CLOUDINARY_API_KEY=...
CLOUDINARY_API_SECRET=...
CLOUDINARY_FOLDER=uploads
CLOUDINARY_UPLOAD_PRESET=CLOUDINARY_CLOUD_NAME: Cloudinary account cloud name.CLOUDINARY_API_KEY: Cloudinary API key.CLOUDINARY_API_SECRET: Cloudinary API secret.CLOUDINARY_FOLDER: destination folder for uploads.CLOUDINARY_UPLOAD_PRESET: optional preset for direct uploads.
API Reference
- Proxy mode:
POST /api/uploads: upload multipart file through your API server.- Direct mode:
POST /api/uploads/presign: create signed upload URL/params.POST /api/uploads/complete: finalize direct upload and persist metadata.- Large mode (S3/R2):
POST /api/uploads/multipart/init: start multipart upload.POST /api/uploads/multipart/complete: complete multipart upload.POST /api/uploads/multipart/abort: abort multipart upload.- All modes:
POST /api/uploads/access-url: get access URL for stored key.DELETE /api/uploads: delete uploaded file and metadata.
Advanced Usage
Private mode:
UPLOAD_AUTH_MODE=required
UPLOAD_FILE_ACCESS=privateCustom proxy route integration:
import { Router } from "express";
import { uploadSingle } from "../lib/uploads/proxy";
import { saveProxyUpload } from "../lib/uploads";
const router = Router();
router.post("/products", uploadSingle("image"), async (req, res) => {
const result = await saveProxyUpload(req.file!, { ownerId: "user_123" });
res.status(201).json({ imageKey: result.upload.key });
});Database metadata integration:
npx zuro add database
npx zuro add uploadsThis adds src/db/schema/uploads.ts and stores upload records in your database.
Example Use Cases
- User profile/avatar uploads with private access URLs.
- Direct browser uploads for large media-heavy applications.
- Document management APIs with MIME and file-size enforcement.
- Webhook-driven ingestion pipelines that persist upload metadata.