Images
@fluixi/start ships an image pipeline: a route that resizes and re-encodes on demand,
and a loader that points image markup at it.
// src/api/image.ts → /api/image
import { createImageHandler } from '@fluixi/start';
export const GET = createImageHandler({
domains: ['images.example.com'],
});
import { imageLoader } from '@fluixi/start';
const loader = imageLoader(); // defaults to /api/image
<Image src="/mug.jpg" loader={loader} widths={[120, 240, 480]} />;
The handler reads url, w and optional q from the query string, so the loader's only
job is building that URL — which means anything that can produce a srcset can use it.
Configuring the handler
export const GET = createImageHandler({
root: './public', // where local /foo.jpg sources are read from
domains: ['images.example.com'],
sizes: [320, 640, 1280],
quality: 80,
cacheMaxAge: 60 * 60 * 24 * 365,
cache: 100,
});
Every option has a defensible default; two are worth setting deliberately.
domains is an SSRF guard. An image endpoint that fetches whatever URL it is given is
a way to make your server issue arbitrary requests — including to internal addresses a
browser could never reach. Absolute sources are only fetched from hostnames you list, so
leaving it unset means only local files are allowed. That is the safe default, and the
reason to widen it consciously.
sizes bounds the work. A request for a width outside the list is rejected rather than
resized, so a crawler walking ?w=1, ?w=2, ?w=3 cannot make your server encode
thousands of variants and fill the cache. The default list covers common device widths.
quality is both the default and the ceiling — a request asking for more than the
configured quality does not get it.
Encoding
Transformation uses sharp when it is installed, and passes the original through
untouched when it is not. So the pipeline degrades to plain file serving rather than
failing, and adding sharp is what switches optimisation on:
pnpm add sharp
Supply your own transform to use a different codec.
Caching
Responses are sent with a long-lived immutable Cache-Control, which is safe because the
URL encodes the transformation — a different width or quality is a different URL. In front
of that sits a small in-memory cache of recent results (100 entries by default), so a
repeated request does not re-encode. Set cache: false to disable it, or a number to size
it.
In production most requests should never reach the handler at all: a CDN in front of it will serve the immutable responses itself.
Next: Deployment.