A secure REST API for file uploads with type/size validation, dual storage (local disk + Cloudinary), and interactive Swagger documentation.
Level: 3 · Status: ✅ Built · Live Demo · Source Code
File uploads are tricky — you need to handle multipart form data, validate file types (prevent executable uploads), enforce size limits (prevent disk exhaustion), and choose a storage strategy (local vs cloud). This project implements both local disk and Cloudinary storage, teaching you the patterns for any file upload scenario.
- Runtime: Node.js
- Framework: Express 5
- Database: None (file metadata in response only)
- File handling: Multer (multipart parsing + local storage)
- Cloud storage: Cloudinary + multer-storage-cloudinary
- IDs: uuid
- Security: Helmet 8, express-rate-limit, CORS
- Documentation: Swagger (swagger-jsdoc + swagger-ui-express)
- Deployment: Render.com
-
Configure Multer for local storage. Set up Multer with disk storage: define destination directory, generate unique filenames with uuid, and configure size limits (5MB default). Multer parses multipart/form-data and makes the file available as
req.file. -
Add file type validation. Create a file filter function that checks MIME type. Allow only: images (jpeg, png, gif, webp) and documents (pdf, docx). Reject everything else with a clear error message. Never trust the file extension alone — check the MIME type.
-
Configure Cloudinary storage. Set up multer-storage-cloudinary as an alternative storage engine. Same Multer interface, but files go directly to Cloudinary instead of local disk. Return the Cloudinary URL in the response.
-
Build upload endpoints.
POST /upload/localstores on disk, returns file path.POST /upload/cloudstores on Cloudinary, returns URL. Both validate type and size. Return metadata: filename, size, mimetype, url/path. -
Add size limit enforcement. Multer's
limits: { fileSize: 5 * 1024 * 1024 }rejects oversized files. Handle theLIMIT_FILE_SIZEerror with a user-friendly message. Different endpoints can have different limits. -
Build a health check and docs.
GET /healthfor uptime monitoring. Swagger documents both upload endpoints with multipart/form-data request body, file field name, and response schema. -
Deploy to Render. Set
CLOUDINARY_CLOUD_NAME,CLOUDINARY_API_KEY,CLOUDINARY_API_SECRETfor cloud uploads. Local uploads use Render's disk (ephemeral — files lost on redeploy).
Deploy on Render.com. Cloudinary credentials as env vars. Local storage is ephemeral on Render — use Cloudinary for persistent file storage in production.
- Never trust file extensions. A user can rename
malware.exetoimage.jpg. Always validate the MIME type (Content-Type header from Multer) and optionally use magic bytes (file signature) for deeper validation. - Render's disk is ephemeral — files are lost on each deploy. This makes Cloudinary the correct production choice. Local disk is only useful for development or temporary processing.
- Extension: add image resizing (sharp library), multiple file upload, presigned URLs for direct-to-cloud upload, or file metadata storage in a database.
The project repo's README should include a description, upload endpoint docs with curl examples, supported file types, size limits, tech stack, and setup instructions.