You're building an API endpoint that accepts images. You have three main approaches, and most devs pick the wrong one. Let's fix that.
1. Base64 Encoding (The Lazy Way)
Base64 converts binary data to text. Yes, it was useful for email attachments in 1995 when SMTP servers choked on binary data. Today? It's mostly a bad idea.
The reality: Base64 inflates your payload by ~33%. That 3MB image becomes 4MB. Your API parsing that JSON? It's now handling massive strings. Your database storing these strings? Enjoy the bloat.
When it's actually okay: Tiny icons or SVGs under 10KB embedded in CSS/HTML. That's it. If you're sending actual images this way, you're doing it wrong.
2. Multipart/Form-Data (The Standard Way)
This is how HTML forms have sent files since forever. The catch? Everything that isn't a file becomes a string.
// What you send
formData.append('file', imageFile);
formData.append('metadata', JSON.stringify({ title: 'My Image', tags: ['nature'] }));
The problem: Your nice DTOs and validation decorators in NestJS? Useless. You're parsing strings and validating manually. That metadata
field? You're JSON.parse()
-ing it and praying it's valid.
When to use it: Simple uploads with minimal metadata. Profile picture update? Sure. Complex nested objects with files? Pain.
3. Separate Endpoints (The Clean Way)
Upload files to one endpoint, get back IDs, then send those IDs with your JSON payload to another endpoint.
// Step 1: Upload image
POST /api/uploads
Response: { id: "abc123", url: "https://..." }
// Step 2: Create resource with image reference
POST /api/posts
Body: { title: "My Post", imageId: "abc123", ... }
The tradeoff: Yes, orphaned files. Write a cleanup job that runs every hour and deletes uploads older than 24 hours without associations. It's 20 lines of code, not rocket science.
Why this wins:
- Clean separation of concerns
- DTOs work normally
- Better caching strategies
- Resumable uploads become possible
- Can optimize file handling separately (CDN, processing queues)
The Production Reality
Stop overthinking this. For 90% of apps, use separate endpoints. For simple forms with a single file, use multipart. Never use base64 for actual files unless you enjoy angry users and slow APIs.
And please, implement proper file validation. Check MIME types, file sizes, and actually validate the file content. Users will upload anything.
Questions?
Got a different approach? Think I'm wrong about base64? Running into edge cases I didn't cover? Drop a comment. And if you're still sending 10MB PNGs as base64 strings in 2025, we need to talk.
Top comments (1)
The orphaned files tradeoff ain't worth it.
Your create image post can just as well handle the creation of the post.
An endpoint to create an object can handle the image just fine, just write your code with separation of concerns and everything would work just fine and till be modular and clean.
Your preference is not a standard.
It's not a bad approach, but not having to write a cron to clean things up goes a long long way as far as tech debt goes. Always think about future you not having to remember that there's a cron that has to clean things up because your code didn't just handle it from the get go.