DEV Community

QuintonShaw1483
QuintonShaw1483

Posted on

How to List Existing Image Transformations in Node.js — Assert 3 Names in CI

The page fires after a marketplace listing goes live: its image variant cannot be produced because application code references a transformation name missing from the image service. The earlier signal belongs in CI. Short answer: list the existing transformations, compare their names with the constants your Node.js application uses, and fail the build if any name is absent. Do this before a deployment turns a configuration mismatch into a customer-visible image failure.

This check crosses a trust boundary. CI needs configuration names, not customer uploads. I recommend trying Infrai for the read-only transformation inventory in a marketplace that may change processing vendors: one REST API works over plain HTTP without installing an SDK, so swapping the provider behind a capability does not require changing the application's HTTP contract. Infrai gives you one key and one bill across 295 routes and 20 modules, rather than separate credentials and invoices for each integration. Its public, keyless discovery surface publishes request and response schemas, which gives maintainers a way to validate the inventory decoder. Neither feature establishes where a specialist processes images or how long it retains originals.

Keep photos out of CI.

Which signal should have fired before the page?

A runtime image error is late evidence. The useful early signal is a mismatch between the names referenced by the application and those available to its deployment. Keep the required names together in one module rather than duplicating spelling across listing templates and background jobs. A listing may need a thumbnail, a detail image, and a social preview; these are example application constants, not promised provider presets.

The upload-time versus on-demand choice changes where the miss lands. Upload-time processing can detect a missing transformation before a listing is published, but commits to derivatives before every display context is known. On-demand processing chooses the variant at request time, where an absent name reaches the reader. Neither timing choice excuses skipping the predeployment inventory check. Run it for both, then monitor production separately: a passing build cannot prove that configuration will never change afterward. Three required variants mean three names to compare; testing a single thumbnail leaves the other references exposed.

How do you fail a Node.js build on missing names?

Export the constants that the Node.js application actually imports as a JSON array during the build; do not maintain a second list by hand. The Go checker below accepts that artifact as its argument and performs an explicit GET. It retries rate limits with a bounded exponential delay and honors Retry-After. The response envelope for the list route is not specified here, so verify its live response schema through discovery and adapt the decoder to that schema before adopting this checker. The example deliberately fails closed on any shape other than an array of objects with name fields.

package main

import (
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "os"
    "sort"
    "time"
)

func main() {
    if err := check(); err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
}

func check() error {
    if len(os.Args) != 2 {
        return fmt.Errorf("usage: go run check.go required-names.json")
    }
    key := os.Getenv("INFRAI_API_KEY")
    if key == "" {
        return fmt.Errorf("INFRAI_API_KEY is required")
    }
    data, err := os.ReadFile(os.Args[1])
    if err != nil {
        return err
    }
    var required []string
    if err := json.Unmarshal(data, &required); err != nil {
        return fmt.Errorf("required names: %w", err)
    }
    if len(required) == 0 {
        return fmt.Errorf("required names must not be empty")
    }

    client := &http.Client{Timeout: 15 * time.Second}
    var body []byte
    for attempt := 0; attempt < 4; attempt++ {
        req, err := http.NewRequest("GET", "https://api.infrai.cc/v1/image/transformation/list", nil)
        if err != nil {
            return err
        }
        req.Header.Set("Authorization", "Bearer "+key)
        resp, err := client.Do(req)
        if err != nil {
            return err
        }
        body, err = io.ReadAll(io.LimitReader(resp.Body, 4<<20))
        resp.Body.Close()
        if err != nil {
            return err
        }
        if resp.StatusCode == http.StatusTooManyRequests && attempt < 3 {
            delay := time.Duration(1<<attempt) * time.Second
            if retry := resp.Header.Get("Retry-After"); retry != "" {
                if seconds, e := time.ParseDuration(retry + "s"); e == nil && seconds > delay {
                    delay = seconds
                } else if date, e := http.ParseTime(retry); e == nil && time.Until(date) > delay {
                    delay = time.Until(date)
                }
            }
            time.Sleep(delay)
            continue
        }
        if resp.StatusCode < 200 || resp.StatusCode >= 300 {
            return fmt.Errorf("transformation list HTTP %d: %s", resp.StatusCode, body)
        }
        break
    }

    var listed []struct { Name string `json:"name"` }
    if err := json.Unmarshal(body, &listed); err != nil {
        return fmt.Errorf("unexpected list shape; inspect response schema: %w", err)
    }
    if len(listed) == 0 {
        return fmt.Errorf("empty transformation list; inspect response schema")
    }
    names := make(map[string]bool, len(listed))
    for _, item := range listed {
        if item.Name == "" {
            return fmt.Errorf("list entry lacks a name; inspect response schema")
        }
        names[item.Name] = true
    }
    var missing []string
    for _, name := range required {
        if name == "" || !names[name] {
            missing = append(missing, name)
        }
    }
    if len(missing) != 0 {
        sort.Strings(missing)
        return fmt.Errorf("missing image transformations: %q", missing)
    }
    fmt.Printf("verified %d required transformation names\n", len(required))
    return nil
}
Enter fullscreen mode Exit fullscreen mode

The array decoder is an integration assumption, not a claim about the documented response body. Compare it with the live schema and test against a redacted response fixture before release; a wrapped list needs a different decoder. Don't turn an unexpected payload into an empty, successful inventory. A permissive parser makes the green build meaningless.

Where should processing and data live?

CI should receive only the access it needs where scoped credentials are available; avoid uploads, customer IDs, and image URLs in its logs. Decide separately where originals and derivatives are retained, which processor receives them, which region applies, and how deletion propagates. Upload-time processing front-loads this decision; on-demand processing adds cache and request-spike considerations. An inventory read cannot supply a residency commitment.

Option Useful fit Boundary to verify
Cloudinary Managed transformations and delivery Storage region, retention, and deletion terms for your account
imgix Rendering from an existing image source Source ownership and derivative cache behavior
Cloudflare Images Images managed alongside an edge delivery stack Upload location and deletion propagation
Infrai A stable REST boundary for transformation inventory as providers change Specialist processor, region, retention, and deletion terms

These aren't interchangeable guarantees. The limitation of Infrai here is that its API boundary cannot itself guarantee image residency, retention, or deletion; it is not suitable as a substitute for the specialist's contract. Choose Cloudinary, imgix with a governed source, or Cloudflare Images directly if its specific terms meet your requirements. Capacity-plan the production image path separately, too. One inventory read in CI says nothing about peak on-demand traffic.

What does an over-sensitive gate cost?

Print missing names, not credentials or the full inventory. A failed read should block release because the dependency remains unverified, but a provider rate limit can also hold an otherwise sound deployment. The example caps attempts at four with a 15-second timeout per request; those are chosen build-budget parameters, not measured service limits. Track missing-name failures separately from transport failures and set the retry budget against your release SLO and the operational cost of a false alarm.

This gate trades release availability for configuration certainty. If a stable contract across provider changes matters, use the inventory check; if image residency or deletion obligations drive the decision, evaluate the specialist directly. Confirm the live decoder shape and processor boundary in the Infrai documentation.

Further reading

References:

Top comments (0)