Skip to content
Go back

Building Image Optimizer: From Personal Tool to Developer API

Updated: (Originally published 16 Oct, 2025 )

TL;DR: Built a production-ready image optimization service with Go + libvips, featuring a Next.js UI, CLI tool, spritesheet packing with game engine support, API key authentication, rate limiting, and comprehensive Swagger documentation. Zero server storage, fully ephemeral processing, live at sosquishy.io.

Like most side projects, this one started with frustration.

We started this to help my son out, who was working on images for his own projects and trying to squeeze every byte out he could from the loading process. He kept running into the same limitations: subscription walls, file size caps, or tools that destroyed image quality. I needed something better, something I could control, and something that respected my privacy.

So I built it.

The Goal

Started simple: take any image, make it smaller and faster without sacrificing quality. Then I saw what libvips could do — the performance, the quality, the flexibility — and realized this could be more.

It grew into:

All while maintaining a core principle: zero server-side storage. Images are processed entirely in-memory and discarded immediately. No logs, no history, no tracking.

Tech Stack: Why Go + libvips?

The Backend: Go + Fiber

I chose Go for the API backend for several reasons:

  1. Performance: Go’s concurrency model handles multiple image processing requests efficiently
  2. libvips integration: The bimg library provides excellent Go bindings for libvips
  3. Deployment simplicity: Single binary, small Docker images, minimal runtime dependencies
  4. Type safety: No runtime surprises when handling API parameters

The Fiber framework provided a familiar Express-like API with Go’s performance characteristics.

Why libvips?

Initially, I considered ImageMagick (via Go bindings), but libvips proved superior:

The combination of libvips + oxipng for PNG post-processing delivers an additional 15-40% compression on top of libvips’ already excellent PNG optimization.

The Frontend: Next.js + TypeScript

The web UI needed to be fast, responsive, and easy to deploy. Next.js provided:

Key UI features:

Architecture and Features

Image Processing Core

The service supports multiple optimization strategies:

Formats Supported:

Processing Features:

API Design

The REST API exposes multiple endpoints for different use cases:

# Single image optimization (file upload or URL)
POST /optimize
Content-Type: multipart/form-data
Authorization: Bearer YOUR_API_KEY

# Batch optimization (multiple images)
POST /batch-optimize
Content-Type: multipart/form-data
Authorization: Bearer YOUR_API_KEY

# Spritesheet packing (pack multiple sprites into optimized sheets)
POST /pack-sprites
Content-Type: multipart/form-data
Authorization: Bearer YOUR_API_KEY

# Spritesheet optimization (import existing sheet, deduplicate, repack)
POST /optimize-spritesheet
Content-Type: multipart/form-data
Authorization: Bearer YOUR_API_KEY

# Health check
GET /health

Input flexibility:

Security and rate limiting:

CLI Tool: imgopt

For developers who prefer command-line workflows, the imgopt CLI provides batch processing:

# Optimize all images in a directory
imgopt -input ./photos -output ./optimized -quality 85 -format webp

# Custom API endpoint
imgopt -input ./photos -api https://api.sosquishy.io/optimize

Features:

Developer Experience: Swagger/OpenAPI Documentation

Every endpoint is fully documented with interactive Swagger UI at /swagger/index.html.

The documentation includes:

Generated automatically from code annotations using swaggo/swag, ensuring docs stay synchronized with implementation.

Privacy and Security by Design

One of the core principles of this project: your images are your business, not mine.

Zero Server Storage:

Security Measures:

GDPR Compliant:

Deployment and Infrastructure

Frontend: GitHub Pages

API: Render.com (See updates below)

Update (Oct 2024): The API has since been migrated from Render → Fly.io (to address memory constraints) → Hetzner (for cost optimization). See Migrating from Render to Fly.io and Migrating to Hetzner for the full migration story. Current hosting: Hetzner VPS.

Local Development:

Real-World Performance

Testing with typical web images shows significant improvements:

JPEG Optimization:

PNG to WebP Conversion:

PNG + oxipng Post-Processing:

What I Learned

1. libvips is Seriously Underrated

Coming from Node.js where sharp (libvips wrapper) is popular, I still underestimated the performance boost in Go. Streaming architecture + memory efficiency made concurrent requests trivial.

2. Privacy Became a Feature

Not storing images wasn’t just security — it became a selling point. Users care about privacy with potentially sensitive images. Ephemeral processing eliminated entire categories of security concerns.

3. API-First Design Pays Off

Built the API before the UI. This forced better decisions. The web interface became just one client, making the CLI tool trivial to add later. Swagger docs became the single source of truth.

4. Go Deployment is Bulletproof

Single binary, minimal dependencies. Docker images ~50MB. No dependency hell, no runtime mismatches, no surprise breakages.

5. Static Export Simplifies Everything

Next.js static export = no Node.js server needed. Entire frontend is HTML/CSS/JS on GitHub Pages. Zero server maintenance, zero costs, infinite UI scalability.

Recent Updates: Spritesheet Packing for Game Development

After the initial launch, I added comprehensive spritesheet packing capabilities — a feature my son specifically requested for his Friday Night Funkin’ mod development.

Spritesheet Packer

The spritesheet packer uses the MaxRects bin packing algorithm (BSSF heuristic) to achieve 85-95% space utilization when combining multiple sprite images into optimized texture atlases.

Key Features:

Supported Output Formats:

Spritesheet Import & Optimization

For game developers working with existing spritesheets, the optimizer can:

  1. Import existing spritesheets with Sparrow XML metadata
  2. Extract individual frames using XML coordinates
  3. Deduplicate sprites with pixel-perfect byte-level comparison (70-75% reduction on typical FNF spritesheets)
  4. Repack optimally using MaxRects algorithm
  5. Export in any format - convert between game engine formats

Real-world performance:

Testing with Friday Night Funkin’ spritesheets:

This workflow solved a major pain point in FNF modding where artists often create animations with many duplicate frames (idle poses, held notes, etc.), wasting texture memory and loading time.

Why This Matters for Game Development

Texture Memory Optimization:

Cross-Engine Compatibility:

Developer Workflow:

The 8MB upload limit supports even large game spritesheets, and the ephemeral processing means your game assets stay private.

What’s Next

Service is live and production-ready. Some ideas for later:

Planned:

Maybe:

Try It Out

Web UI: sosquishy.io API Docs: api.sosquishy.io/swagger/index.html Source Code: github.com/keif/image-optimizer

You can build a fast, privacy-focused image optimization service without complex infrastructure or expensive third-party services. Go’s performance + libvips’ quality + modern frontend tooling = developer-friendly platform that respects privacy.

If you’re building something that processes images, hopefully this is a useful reference for balancing performance, privacy, and developer experience.


Share this post on:

Previous Post
Cleaning Up GitHub Notifications with Python
Next Post
Goodbye Jib: Modernizing Container Builds for a Simpler CI/CD Workflow