Skip to content
September 4, 20266 min readBy Dzaki Amri Zaidaan

Hacking Image Uploads for the Forem API: Inside devpub's First Community PR

Learn how devpub v0.3 added image uploads from the terminal despite the Forem API lacking an endpoint. We'll dissect the workaround, the architecture, and the story of the first external contributor.

#Frontend
Computer screen displaying lines of code

The Problem & Industry Shift

The developer blogging ecosystem has seen a surge in CLI tools that let you publish content without leaving the terminal. Tools like dev.to's own API clients, medium-cli, and static site generators with CMS integrations are part of a broader shift toward developer experience (DX) that values speed and context-switching reduction. However, these tools often hit a wall when they need to upload images.

Forem, the open-source platform behind DEV Community, provides a comprehensive REST API for articles, comments, and users [1]. But as of this writing, there is no public endpoint for uploading images. This is a significant gap because images are essential for rich, engaging articles. When I built devpub, a Python CLI for publishing to DEV, I knew that image support would be a dealbreaker for many users.

This article tells the story of how devpub v0.3 added image uploads from the terminal, the clever workaround we used, and the first community PR that made it all possible.

Architecture & Core Mechanics

The Workaround: Upload to a Third-Party Host

Since Forem doesn't offer an image upload API, we had to leverage an external service. The most straightforward approach is to upload images to a free image host that provides a direct URL, then embed that URL in the article's markdown. After evaluating options like Imgur, Cloudinary, and GitHub Gists, we chose Imgur for its simplicity and generous anonymous API limits.

The flow is as follows:

+-----------+    1. Upload image     +--------+    2. Get URL     +---------+
|  devpub   | ---------------------> |  Imgur  | <--------------> |  Imgur  |
|  CLI      |                        |  API    |                  |  CDN    |
+-----------+                        +--------+                  +---------+
     |                                                                  |
     | 3. Embed URL in markdown                                         |
     v                                                                  |
+-----------+                                                          |
|  Article  | -----------------------------------------------------------+
|  Markdown |
+-----------+

Why Not Use GitHub or a Personal Server?

  • GitHub Gists: Could work but requires authentication and has rate limits. Also, gist URLs are not ideal for hotlinking.
  • Cloudinary: More powerful but requires an account and API key, adding friction.
  • Personal server: Not feasible for most users.

Imgur's anonymous API allows up to 50 uploads per hour per IP, which is plenty for a typical article. The trade-off is that images are public and subject to Imgur's terms, but for most use cases this is acceptable.

The Implementation

We added a new command devpub upload that accepts a file path or a directory of images. The core logic is in a new module imgur.py:

# imgur.py
import base64
import os
import requests
from typing import Optional

IMGUR_API_URL = "https://api.imgur.com/3/image"

def upload_image(file_path: str, client_id: str) -> Optional[str]:
    """Upload an image to Imgur and return the direct link."""
    # Read the file and encode it in base64
    with open(file_path, "rb") as f:
        image_data = base64.b64encode(f.read())

    headers = {"Authorization": f"Client-ID {client_id}"}
    data = {"image": image_data, "type": "base64"}

    response = requests.post(IMGUR_API_URL, headers=headers, data=data)
    if response.status_code == 200:
        return response.json()["data"]["link"]
    else:
        # Log the error and return None
        print(f"Upload failed: {response.status_code} - {response.text}")
        return None

In the CLI, we added a command that processes one or more files and prints the resulting URLs, which the user can then paste into their article. But we wanted a smoother experience: automatically replacing local image references in markdown with the uploaded URLs.

The First Community PR

I had open-sourced devpub on GitHub, and within a week, a developer named @johndoe (pseudonym) submitted a PR that implemented exactly that feature. The PR added a --replace flag that scans a markdown file for local image references like ![alt](images/foo.png) and replaces them with the uploaded URL.

The PR was well-structured, with tests and documentation. It was a perfect example of a focused, high-quality contribution. After a few rounds of review, we merged it.

Production Code Example

Here's the final implementation of the upload command with the --replace flag, as contributed by the community:

# cli.py (excerpt)
import click
import re
from pathlib import Path
from .imgur import upload_image

@click.command()
@click.argument('files', nargs=-1, type=click.Path(exists=True))
@click.option('--client-id', envvar='IMGUR_CLIENT_ID', required=True, help='Imgur API client ID')
@click.option('--replace', is_flag=True, help='Replace local image references in markdown files with uploaded URLs')
def upload(files, client_id, replace):
    """Upload images to Imgur and print URLs."""
    for file in files:
        url = upload_image(str(file), client_id)
        if url:
            click.echo(f"{file}: {url}")
            if replace:
                # Find all markdown files in the current directory
                for md_file in Path('.').glob('*.md'):
                    content = md_file.read_text()
                    # Replace local image references with the uploaded URL
                    # Pattern matches ![alt](path)
                    pattern = r'!\[([^\]]*)\]\((' + re.escape(file) + r')\)'
                    content = re.sub(pattern, lambda m: f'![{m.group(1)}]({url})', content)
                    md_file.write_text(content)
                    click.echo(f"Updated {md_file}")

Key engineering decisions:

  • Environment variable for client ID: Keeps secrets out of the command line history.
  • Regex for replacement: Handles spaces and special characters in filenames.
  • Batch processing: Supports multiple files and updates all markdown files in the directory.

Performance, Cost & Trade-offs

Performance

  • Upload speed: Imgur's API typically responds in under 500ms for images under 1MB. For larger images, it can take a few seconds.
  • Rate limits: Anonymous uploads are limited to 50 per hour per IP. For a typical article with 5-10 images, this is fine. But if you're uploading a large batch, you might hit the limit.

Cost

  • Imgur: Free for anonymous uploads, but images are public and subject to removal if they violate terms. For most developers, this is acceptable.
  • Alternative: Cloudinary offers a free tier but requires an account and has more complex API.

Trade-offs

  • Dependency on a third-party service: If Imgur goes down or changes its API, the feature breaks. We mitigate this by isolating the upload logic in a module that can be swapped out.
  • Privacy: Images are public by default. If you need private images, you'd need a different solution.
  • URL stability: Imgur links are generally stable, but there's no guarantee. For critical images, consider self-hosting.

Actionable Checklist / Summary

When adopting a similar approach in your own tools, consider the following:

  1. Evaluate third-party services based on your needs: rate limits, privacy, cost, and reliability.
  2. Isolate the upload logic behind an interface so you can swap providers later.
  3. Handle errors gracefully: Provide clear messages when uploads fail.
  4. Consider security: Never hardcode API keys; use environment variables or config files.
  5. Automate the workflow: If your tool generates markdown, offer a flag to automatically replace local image references.
  6. Encourage community contributions: Open-source your tool and make it easy for others to contribute. A well-documented codebase and a clear contributing guide can attract high-quality PRs.

References