Blog Technical 13 min read

Building a Self-Hosted Placeholder Image API with Cloudflare Workers

A practical tutorial for building a self-hosted SVG placeholder image API on Cloudflare Workers, with routing, escaping, cache headers, and production tradeoffs.

Self hosted placeholder image APICloudflare Workers image APISVG placeholder APIBuild placeholder image API
Building a Self-Hosted Placeholder Image API with Cloudflare Workers

A self-hosted placeholder image API can be built with a Cloudflare Worker that parses dimensions, generates constrained SVG, escapes text, and returns cacheable image responses.

Self-hosting gives infrastructure control, but it also means owning validation, caching, abuse prevention, compatibility, monitoring, and framework documentation that fallback.pics already handles.

Search intent

Should you self-host a placeholder image API?

Developers searching for a self-hosted placeholder image API usually want control: internal URLs, private infrastructure, predictable SVG output, custom colors, or a simple service for staging and docs.

That is a valid path if your team is prepared to own the operational details. A placeholder API looks small, but production use quickly involves URL parsing, cache behavior, input escaping, rate limiting, monitoring, and framework-specific usage patterns.

Self-host when

You need internal-only infrastructure, custom routing, strict platform ownership, or a learning project.

Use fallback.pics when

You want copy-paste placeholder URLs without maintaining a Worker, cache policy, docs, and abuse controls.

Hybrid path

Use fallback.pics in product apps and keep a tiny self-hosted worker for experiments or internal tooling.

Architecture

The moving parts of a Worker-based SVG API

A minimal Cloudflare Workers placeholder API has four jobs: parse the request URL, validate dimensions and colors, generate safe SVG, and return a Response with image and cache headers.

Keep the first version small. Add formats, presets, avatars, skeletons, analytics, and custom domains only after the core route is safe and cacheable.

Implementation text
Request URL
  -> parse /api/v1/800x600
  -> validate width, height, colors, and text
  -> generate escaped SVG
  -> return image/svg+xml with cache headers

Route shape

Use a predictable URL format

Start with a single deterministic route shape. The fallback.pics canonical generated image route is /api/v1/[width]x[height], with optional colors and text.

Readable URL segments make it easier to debug cache keys and reuse the same placeholder across docs, fixtures, tests, and production fallback components.

Implementation text
https://fallback.pics/api/v1/800x600
https://fallback.pics/api/v1/800x600/F4F4F5/18181B?text=Product+Image
https://fallback.pics/api/v1/avatar/96?text=User
https://fallback.pics/api/v1/skeleton/1200x630

Worker code

A minimal Cloudflare Worker placeholder API

Cloudflare Workers receive requests in a fetch handler and return Web standard Response objects. That is enough for a basic SVG placeholder endpoint.

This example is intentionally small. It supports /api/v1/800x600 and optional text, then returns deterministic SVG with long-lived cache headers.

Implementation tsx
const dimensionPattern = /^\/(?:api\/v1\/)?(\d+)x(\d+)$/;

function escapeXml(value: string) {
  return value
    .replace(/&/g, '&')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#39;');
}

function svg(width: number, height: number, text: string) {
  const label = escapeXml(text || width + ' x ' + height);
  const fontSize = Math.max(14, Math.min(width, height) * 0.1);

  return '<svg xmlns="http://www.w3.org/2000/svg" width="' + width + '" height="' + height + '" viewBox="0 0 ' + width + ' ' + height + '">' +
    '<rect width="100%" height="100%" fill="#7C3AED"/>' +
    '<text x="50%" y="50%" fill="#FFFFFF" font-family="system-ui" font-size="' + fontSize + '" text-anchor="middle" dominant-baseline="middle">' + label + '</text>' +
    '</svg>';
}

export default {
  async fetch(request: Request): Promise<Response> {
    const url = new URL(request.url);
    const match = url.pathname.match(dimensionPattern);

    if (!match) {
      return new Response('Invalid placeholder route', { status: 400 });
    }

    const width = Number(match[1]);
    const height = Number(match[2]);

    if (width < 10 || height < 10 || width > 4000 || height > 4000) {
      return new Response('Invalid dimensions', { status: 400 });
    }

    const body = svg(width, height, url.searchParams.get('text') || '');

    return new Response(body, {
      headers: {
        'Content-Type': 'image/svg+xml',
        'Cache-Control': 'public, max-age=31536000, immutable',
        'CDN-Cache-Control': 'max-age=31536000',
        'X-Content-Type-Options': 'nosniff'
      }
    });
  }
};

Validation

Validate dimensions and colors before generating SVG

Do not let arbitrary path segments become SVG markup. Treat all request input as data. Validate dimensions, restrict colors to known formats, and escape text before inserting it into an SVG response.

Dimension limits also protect your service. A request for a tiny or enormous SVG should fail before generation.

Implementation tsx
const maxSize = 4000;
const hexColorPattern = /^[0-9A-Fa-f]{6}$/;

function normalizeColor(value: string, fallback: string) {
  const cleaned = value.replace('#', '');
  return hexColorPattern.test(cleaned) ? '#' + cleaned : fallback;
}

function isValidSize(value: number) {
  return Number.isInteger(value) && value >= 10 && value <= maxSize;
}

Escaping

Escape text labels every time

The text query parameter is the highest-risk part of a basic SVG placeholder API because it is user-controlled and appears inside XML. Escape it every time, even if you also limit length.

Do not accept raw SVG, HTML, scripts, event handlers, or arbitrary attributes as API input. A placeholder API should generate markup from a constrained template.

Implementation tsx
function escapeXml(value: string) {
  return value
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#39;');
}

Cache headers

Return SVG with cacheable image headers

A deterministic placeholder URL is a good candidate for long-lived caching. Use the URL as the version: if dimensions, colors, or label change, the URL changes.

Set Content-Type to image/svg+xml and include nosniff. Use Cache-Control and CDN-Cache-Control when you want both browser and CDN caches to keep stable placeholders.

Implementation text
headers: {
  'Content-Type': 'image/svg+xml',
  'Cache-Control': 'public, max-age=31536000, immutable',
  'CDN-Cache-Control': 'max-age=31536000',
  'X-Content-Type-Options': 'nosniff'
}

Cache API

Use the Worker Cache API only when you need manual control

For many placeholder APIs, response cache headers and normal CDN behavior are enough. The Workers Cache API is useful when you need programmatic cache reads, writes, purges, or custom cache keys.

Manual cache code adds complexity. If you use it, remember that cache keys include the URL, and unique query strings can fragment the cache just as they do in normal CDN caching.

URL cardinality

Avoid unlimited unique placeholder URLs

A self-hosted API can accidentally create an unbounded cache problem. If users put product names, emails, IDs, timestamps, or request-specific labels into text, every request becomes a new cache key.

For production fallback states, prefer generic labels such as Product Image, User, Article Image, Preview, and Image Unavailable.

Implementation text
// Good repeated cache keys
https://fallback.pics/api/v1/800x800?text=Product+Image
https://fallback.pics/api/v1/avatar/96?text=User

// Avoid request-specific URL text

Production concerns

What self-hosting makes you responsible for

The basic Worker is only the start. If the API becomes part of product UI, you need monitoring, abuse protection, route compatibility, cache policy, CORS behavior, tests, docs, and migration rules.

You also need to decide how to handle unsupported formats, invalid dimensions, transparent backgrounds, color contrast, accessibility labels, and framework-specific behavior for React, Next.js, CMS fields, and static docs.

Reliability

Fallback images should be boring and available because they appear when other media fails.

Compatibility

Document how URLs work in img tags, Markdown, CMS fields, React components, and Next.js images.

Abuse prevention

Limit dimensions, text length, request methods, formats, and dynamic input surface.

Observability

Track error rates, top routes, cache misses, and unexpected high-cardinality query patterns.

Decision

Self-hosted Worker vs fallback.pics

Self-hosting is useful when infrastructure control is the main requirement. You can own the domain, code, routing, headers, and deployment pipeline.

fallback.pics is useful when the goal is to stop showing broken images quickly. It gives you deterministic /api/v1 URLs, SVG placeholders, avatars, skeleton states, docs, and framework guides without maintaining a Worker.

Choose self-hosting

For internal-only networks, custom compliance requirements, unusual routing, or platform-learning projects.

Choose fallback.pics

For production fallback states, docs, demos, tests, product placeholders, avatars, and quick integration.

Use both

Prototype self-hosting while keeping fallback.pics as the stable implementation path for product surfaces.

Internal links

Where to go next

Use the self-hosted landing page if you want the shorter product overview. Use the SVG and Cache-Control articles for the two most important technical foundations.

For implementation in apps, use the HTML, React, and Next.js fallback guides.

Implementation text
Self-hosted placeholder API: https://fallback.pics/self-hosted-placeholder-image-api/
SVG placeholder images: https://fallback.pics/blog/svg-placeholder-images-fast-cacheable-scalable/
Cache-Control guide: https://fallback.pics/blog/cache-control-placeholder-images-cdn-browser/
Placeholder image API: https://fallback.pics/placeholder-image-api/
Broken image fallback: https://fallback.pics/broken-image-fallback/
React image fallback: https://fallback.pics/guides/react-image-fallback/
Next.js image fallback: https://fallback.pics/guides/nextjs-image-fallback/

Key takeaways

What to standardize before shipping

  • A self-hosted placeholder API needs routing, validation, XML escaping, SVG generation, and cache headers.
  • Do not accept arbitrary raw SVG or unescaped user-controlled text.
  • Use deterministic URLs and long cache headers only when the URL fully describes the immutable output.
  • Self-hosting gives control but also creates maintenance, monitoring, documentation, and abuse-prevention responsibilities.
  • fallback.pics is the lower-maintenance option when you just need reliable generated fallback image URLs.

Production fallback layer

Use fallback.pics anywhere an image URL is accepted.

Start with one deterministic URL and standardize fallback behavior across your design system.

Read the docs