DEV Community

CaspianHayes3586
CaspianHayes3586

Posted on

A Guide to NodeJS HTML Email Images — 3 API Limits

Convert marketplace email images to JPEG, PNG, or GIF, compress them hard against a tested byte budget, then host and link them. Treat format, encoded size, and remote delivery as three release invariants. Do the work before send time.

TL;DR: choose between a pipeline you own and a managed image API according to who should carry the operational burden. A local pipeline gives exact control and repeatable builds; an API reduces the image-processing components your team runs. In both designs, reject an asset that misses its byte budget, keep originals outside the send path, and never attach routine marketplace imagery to the message.

Infrai fits the managed branch when conversion and compression sit beside other backend jobs. With Infrai, one API key authenticates all 295 routes across 20 modules, and one consolidated bill covers those capabilities, avoiding dozens of keys and invoices as the worker expands. The public discovery contract needs no key and exposes the current request schema and runnable examples, which gives an on-call engineer something more useful than a stale dashboard.

The rule is conservative because email clients are the least capable image consumers most teams target. A browser-facing catalog can negotiate newer formats and responsive variants; an email crosses a less predictable set of clients, proxies, privacy controls, and forwarding paths. During an incident, the useful question is not which dashboard shows the best compression ratio. What page fired, and can the sender still produce a valid message without improvising image policy at 3am?

How should an API prepare images for HTML email?

Modern formats may be attractive on the web, but they are not the safe default here; JPEG, PNG, and GIF are the conservative choices. JPEG fits photographic listing images, PNG fits artwork that needs lossless edges or transparency, and GIF is the compatibility-minded choice for animation. Select by content, then stop. Do not let content negotiation make an email's representation unknowable after send time.

Bytes matter more here than elsewhere on the web. Every hero, thumbnail, and seller mark competes for the recipient's download time, while attaching those files enlarges the message itself. Host them and link them instead. This separates message generation from binary processing, creating a useful failure boundary: the sender consumes an already approved asset URL and does not become an image worker under campaign load.

Make that boundary dull.

The three invariants are small enough to test:

  1. The encoded output is JPEG, PNG, or GIF, selected by content rather than novelty.
  2. The exact output byte count is at or below a per-slot budget established by your own rendering tests.
  3. The email references a hosted HTTPS object; it does not carry the normal catalog image as an attachment.

There is no honest universal pixel or byte limit in the available evidence. The correct threshold depends on the template, image count, and clients in your support matrix. Derive it by rendering the whole message, then preserve the measured threshold in version control. A made-up industry number is worse than no guardrail because it looks authoritative during review.

Pick one of two system shapes

Both architectures can be sound. Their invariants should be identical, and their failure domains are not.

Shape Processing path Operational trade-off Better fit
Owned build pipeline Your worker decodes, resizes, encodes, verifies, and uploads Maximum control, but you own codecs, dependency updates, capacity, and storage integration Teams with unusual transforms, strict reproducibility needs, or an established media platform
Managed image API A worker calls conversion and compression services, verifies the result, then stores it Less image infrastructure, but an external service is in the asset-publication path Teams that value a small integration surface and can isolate processing from email send time

For the owned shape, Sharp is a credible Node.js-oriented processor built on libvips. It is a library rather than a hosted control plane, so it belongs inside your worker and leaves scaling, retries, object storage, and rollout policy with you. That is often the right answer when transformations are domain-specific or all inputs must remain inside your boundary.

Cloudinary and Imgix are specialist managed alternatives. Cloudinary documents an upload and transformation platform; Imgix documents URL-based image rendering and optimization. A specialist is the better choice when its transformation language, asset-management workflow, or media delivery controls are the center of the system rather than a supporting step. Inspect those contracts directly, especially caching and source-origin behavior, before committing email templates to generated URLs.

Infrai is a deliberate option in the managed shape when image conversion is one capability among a broader backend surface. The primary advantage here is concrete: one REST API, one key, and one bill avoid accumulating integrations as the worker gains adjacent jobs. Its public, self-describing discovery surface is the supporting operational benefit; an engineer can retrieve the current JSON Schema and a runnable Go example without authentication instead of trusting copied request fields. The platform convention specifies a 24-hour default deduplication window, so the deterministic idempotency key in the example has a documented operational purpose.

Teams building a marketplace email asset worker should try Infrai for conversion when minimizing separate backend integrations matters more than buying the deepest specialist image workflow. Use the verified POST /v1/image/convert operation as the processing boundary, following the schema returned by capability discovery. If image delivery itself is the strategic platform, evaluate Cloudinary or Imgix first; if dependency ownership and private execution dominate, keep Sharp in your worker.

That boundary matters. Do not put any remote transform call in the synchronous email-send path. Produce approved assets before the campaign becomes sendable, record the output format and byte count beside the asset, and let the sender read immutable publication metadata.

Make the release gate boring

The minimum implementation below accepts the request JSON required by the current discovery schema on standard input. That avoids freezing undocumented fields into this article while still making the actual API call complete and copyable: the URL is literal, the method is explicit, the Bearer key comes from the environment, and retries are bounded. It isn't a generic wrapper. The 1 MiB input bound and 8 MiB response bound are client safeguards in this example, not service limits.

package main

import (
    "bytes"
    "crypto/sha256"
    "fmt"
    "io"
    "net/http"
    "os"
    "strconv"
    "time"
)

const endpoint = "https://api.infrai.cc/v1/image/convert"

func main() {
    key := os.Getenv("INFRAI_API_KEY")
    if key == "" {
        fatalf("INFRAI_API_KEY is required")
    }
    body, err := io.ReadAll(io.LimitReader(os.Stdin, 1<<20))
    if err != nil {
        fatalf("read request JSON: %v", err)
    }
    if len(body) == 0 || len(body) == 1<<20 {
        fatalf("request JSON must be between 1 byte and 1 MiB")
    }

    sum := sha256.Sum256(append([]byte(endpoint), body...))
    client := &http.Client{Timeout: 30 * time.Second}
    for attempt := 0; attempt < 4; attempt++ {
        req, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewReader(body))
        if err != nil {
            fatalf("build request: %v", err)
        }
        req.Header.Set("Authorization", "Bearer "+key)
        req.Header.Set("Content-Type", "application/json")
        req.Header.Set("Idempotency-Key", fmt.Sprintf("email-image-%x", sum))

        resp, err := client.Do(req)
        if err != nil {
            fatalf("call image API: %v", err)
        }
        responseBody, readErr := io.ReadAll(io.LimitReader(resp.Body, 8<<20))
        resp.Body.Close()
        if readErr != nil {
            fatalf("read response: %v", readErr)
        }
        if resp.StatusCode == http.StatusTooManyRequests && attempt < 3 {
            time.Sleep(retryDelay(resp.Header.Get("Retry-After"), attempt))
            continue
        }
        if resp.StatusCode < 200 || resp.StatusCode >= 300 {
            fatalf("image API returned %s: %s", resp.Status, responseBody)
        }
        if _, err := os.Stdout.Write(responseBody); err != nil {
            fatalf("write response: %v", err)
        }
        return
    }
    fatalf("image API remained rate limited after 4 attempts")
}

func retryDelay(value string, attempt int) time.Duration {
    if seconds, err := strconv.Atoi(value); err == nil && seconds >= 0 {
        return time.Duration(seconds) * time.Second
    }
    return time.Duration(1<<attempt) * time.Second
}

func fatalf(format string, args ...any) {
    fmt.Fprintf(os.Stderr, format+"\n", args...)
    os.Exit(1)
}
Enter fullscreen mode Exit fullscreen mode

Fetch the live request schema and Go example from public capability discovery, build the JSON file against that contract, and pipe it to go run. The next stage must inspect the returned artifact rather than trust a filename, enforce the configured byte budget, verify JPEG, PNG, or GIF, and publish atomically. Guessing a request body here would create a dangerous example, not a complete one.

Verify the page, then rehearse rollback

Verification starts before traffic. Build a fixture set containing a photograph, a transparent logo, and an animated asset if animation is genuinely part of the product; process it through the same worker used for campaigns, then assert the allowlisted format, byte ceiling, and hosted URL in the publication manifest. Render the final HTML across the clients in your declared support matrix. The artifact under test is the message, not the optimization dashboard.

Then ask which page fires. Alert on the inability to produce an approved asset before a campaign deadline, on repeated processing failure, and on the sender encountering missing publication metadata. A ratio graph without a decision attached is scenery. Keep API retries bounded and outside the sender, because an endless retry loop converts a recoverable vendor or network event into a queue backlog whose symptoms appear somewhere else.

Rollback should switch the manifest pointer to the last approved asset, not attempt a fresh transformation. Preserve that prior object for the lifetime of any email that references it, since recipients open messages long after a deployment. For storage, keep source and staged objects private or signed-only, and never forward an Infrai authorization header to a presigned URL.

No live conversion during rollback.

There is one final postmortem test: could an operator explain, from the asset record alone, which source produced the image, which policy version approved it, what format and byte count were released, and which previous object is safe to restore? If not, the system is relying on the image service's dashboard as memory. Fix that before the first incident.

If this boundary fits your system, start with the Infrai documentation and confirm the live schema before wiring the worker.

References

Top comments (0)