
Building PixForge: A Self-Hosted Image Upload & Transformation API
How I built a full-stack image upload service with MinIO, Amazon S3, Docker, and Express.js — and what I learned about object storage along the way.
Why I Built PixForge
Image upload is one of those things that sounds simple but gets complicated fast. I wanted to build a service I actually understood end-to-end — not just copy-paste an SDK call.
PixForge is a backend API for image upload, transformation, and retrieval. It supports both MinIO (self-hosted) and Amazon S3 as storage backends, switchable via environment config.
Core Architecture
The stack is deliberately simple:
- Express.js for the API server
- Multer for multipart form handling
- MinIO SDK / AWS SDK v3 for object storage
- Docker for containerization and local MinIO
# Start PixForge with local MinIO
docker-compose up -dUpload Flow
- Client sends
multipart/form-datato/upload - Multer buffers the file in memory
- The file is optionally resized/compressed using
sharp - The processed buffer is streamed to the configured bucket
- A signed URL is returned to the client
app.post('/upload', upload.single('image'), async (req, res) => {
const buffer = await sharp(req.file.buffer)
.resize({ width: 1200, withoutEnlargement: true })
.webp({ quality: 85 })
.toBuffer();
const key = `${Date.now()}-${req.file.originalname}.webp`;
await storageClient.putObject(bucket, key, buffer);
const url = await storageClient.presignedGetObject(bucket, key, 3600);
res.json({ url, key });
});What I Learned
- MinIO is genuinely S3-compatible — swapping the endpoint URL is enough to switch backends.
sharpis fast and produces significant size reductions (often 60–80% smaller).- Docker Compose makes local development with object storage feel effortless.
- Signed URLs are the right pattern for temporary access — don't expose bucket directly.
Read the Full Article
This article is hosted on Hashnode. Read it here →


