Image processing and caching microservice built on Bun and Hono. It reads source images from configured storage backends (S3, FTP, SFTP, LOCAL), applies on-the-fly transformations and returns the processed image. Mostly suitable for batch operations on demand.
- Runtime: Bun
- Framework: Hono
- Image processing: Sharp
- Cache/progress store: Redis (via ioredis)
- Storage backends: S3 (full support), FTP/SFTP/LOCAL (future)
- On-the-fly image transforms via query parameters
- Deterministic naming for processed images (cache-friendly)
- Storage abstraction layer with S3 (now), FTP, SFTP, LOCAL (future) managers
- Batch processing from a directory or an explicit list
- Batch progress tracking via Redis with token-based polling
- Structured error handling with unified JSON responses
- Health/ping routes for readiness checks
- On-demand transformation are not ideal if
- Using remote storage like S3/FTP/SFTP as each fetch takes a lot of time
- You don't get any CDN support that you might get with S3 & compatible storage solutions
- Though it is great if
- Using LOCAL storage backend (future)
- Small app/website that don't heavily rely on lot of users
- CDN support is not very useful for your small use-case
- or you have your own CDN handling
- Bun 1.x
- Node-compatible environment (for sharp and dependencies)
- Redis reachable via
REDIS_URL - At least one storage configured in
config.json - For S3: valid access to bucket and endpoint
bun installThe server expects a config.json at the repository root. Example (S3):
{
"storage": {
"s3_main": {
"type": "s3",
"s3": {
"bucket": "my-bucket",
"endpoint": "https://s3.my-cloud.example",
"accessKey": "AKIA...",
"secretKey": "...",
"prefix": "images/",
"region": "us-east-1",
"convertPath": "converted/",
"acl": "public-read"
}
}
}
}Notes:
s3_main: is basically a unique name of your storage- You can have as many storage backends as you wish (even multiple of the same type even)
prefix: where original images reside (e.g.,images/)convertPath(optional): where processed images will be stored (e.g.,converted/).- When present and reading processed images, the service will look under this path.
- For originals, it uses
prefix. - (RECOMMENDED) to use convertPath as it doesn't mess with the original dir
- S3 credentials are supplied in the S3 block; ACL is required.
Environment variable:
REDIS_URL(required) – e.g.redis://localhost:6379
Create a .env (if you use one) and export before running, or set in your process manager:
export REDIS_URL="redis://localhost:6379"Development:
bun --hot src/index.tsDefault port is 4141 (from src/index.ts).
Health check:
curl -s http://localhost:4141/health | jqBase URL: http://localhost:4141
Also attached the openapi.yaml file which you can copy/paste it on Swagger Editor for the GUI view
-
Health
GET /or/pingor/health- Response:
{ success: true, message: "router is working", version: "..." }
- Response:
-
Image processing
GET /images/:storage/public/:imagePath{.+}:storage: key fromconfig.json(e.g.,s3_main):imagePath: full path including filename and extension relative to storage prefix (e.g.,products/p1.jpg)- Query parameters (validated):
width(number)height(number)quality(0–100)blur(>= 0)rotate(>= 0)greyscale(true/false or 1/0)flip(true/false or 1/0)flop(true/false or 1/0)tint(hex color string, e.g.,#FF0000)format(one of: jpg, jpeg, png, webp, gif, tiff, avif, heif, jp2, jxl, raw)
- Returns: the processed image buffer with proper
Content-Type - Example:
curl -L \ "http://localhost:4141/images/s3_main/public/products/p1.jpg?width=600&height=600&format=webp&quality=80"
-
Batch processing
POST /batch/process/directory- Body JSON:
{ "storageName": "s3_main", "path": "products/", "transformations": { "width": 800, "format": "webp" } } - Starts background batch; returns
{ success, message, token }
- Body JSON:
POST /batch/process/list- Body JSON:
{ "storageName": "s3_main", "filePaths": ["products/p1.jpg", "products/p2.jpg"], "transformations": { "width": 800 } } - Returns
{ success, message, token }
- Body JSON:
GET /batch/progress/:token- Returns
{ success: true, progress: { done, pending, errors: [...] } }
- Returns
-
Configuration utility
GET /config/storage/validate/:storageName- For S3 backends, checks credentials by attempting a list operation.
- Returns
{ success: true, message: "Credentials for 'name' are valid" }on success.
To ensure cacheability and avoid reprocessing,
processed images are saved under convertPath (if configured) using deterministic filenames
derived from the base name and sorted transformation parameters.
Example naming rule (functions.createNameFromParams):
- Original:
p1.jpg - Query:
?width=600&height=800&format=webp - Processed name:
p1-h-800-w-600.webp
On subsequent requests with the same parameters, the service will attempt to read the already-processed file directly.
S3: Full support for upload/read/list/credentials check.FTP/SFTP/LOCAL: upcoming
- Custom
ErrorObjectensures consistent JSON:{ "success": false, "error": "Message" } - Middleware catches and formats unexpected errors to 500.
Common error cases:
- 400: Missing/invalid query parameters, unsupported extension/format, missing file extension.
- 401: Invalid S3 credentials (mapped specific S3 errors).
- 404: Image not found in storage.
- 500/502: Storage or sharp processing issues.
- Ensure
REDIS_URLis set. - Provide
config.jsonwith valid storage configuration. - For S3: verify bucket, endpoint, region, credentials, and network access.
- Expose port
PORT/4141 (or adjust your reverse proxy).
Example Docker snippet (conceptual):
FROM oven/bun:1 as base
WORKDIR /app
COPY package.json bun.lockb ./
RUN bun install --frozen-lockfile
COPY . .
ENV REDIS_URL=redis://redis:6379
EXPOSE 4141
CMD ["bun", "src/index.ts"]- Config file not found or invalid
- The app reads
config.jsonfrom the repo root and validates it with zod. Check logs for detailed path/type errors.
- The app reads
- Redis connection issues
- Ensure
REDIS_URLis reachable; the app waits untilDbRedis.status()becomesready.
- Ensure
- S3 access denied
- Verify credentials, endpoint, region, and bucket policy; use
/config/storage/validate/:storageNameto check.
- Verify credentials, endpoint, region, and bucket policy; use
- Processed image isn't returned
- Confirm you provided at least one valid transformation query parameter; otherwise a 400 is returned.
- LOCAL storage backend
- DOCKER support (with docker-compose)
- Custom headers to return with image
- including Cache-Control headers
- Caching to LOCAL storage (via any backend) for quick image return
- Direct upload/delete image to your storage backend (with admin-token)
- More image transformation operations
- Basic logging support
MIT License
Copyright (c) 2025 CODEVRY LABS (SAKSHAM KHURANA)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.