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.
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.
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.
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.
const dimensionPattern = /^\/(?:api\/v1\/)?(\d+)x(\d+)$/;
function escapeXml(value: string) {
return value
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
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.
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.
function escapeXml(value: string) {
return value
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
} 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.
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.
// 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.
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.