AZ.dev

A storage URL builder and its static file handler must share one prefix constant

When a storage abstraction returns URLs that the app’s static-file handler is supposed to serve, the two must agree on the path prefix. They drift the moment one is changed without the other, and the data they wrote to your DB outlives the fix.

The trap

// Storage factory writes "/uploads/images/foo.png" into every URL it returns.
createLocalImageStorage({
  baseDir: "storage/images",
  publicPath: "/uploads/images",
});

// Static handler serves "/images/*" — a different prefix.
fastify.get("/images/*", serveLocalImage);

Every URL stored in the DB is a 404. Worse, fixing the handler tomorrow doesn’t fix the rows yesterday: any Book.coverImageUrl or markdown ![](url) already persisted carries the broken prefix.

The fix: one constant, two consumers

const IMAGES_PUBLIC_PATH = "/images";

const imageStorage = createLocalImageStorage({
  baseDir: env.IMAGE_DIR,
  publicPath: IMAGES_PUBLIC_PATH,
  publicOrigin: env.IMAGES_PUBLIC_BASE_URL ?? "",
});

fastify.get(`${IMAGES_PUBLIC_PATH}/*`, serveLocalImage);

If you do nothing else, do this. One source for the prefix, both callers import it.

Where audits miss the alignment

A common code audit looks for “the static handler” by grepping route files for the URL prefix the storage layer returns. If your API is mounted under /api/v1 and the static handler is mounted at root (so it serves /images/* directly, not /api/v1/images/*), the grep for "/images" will hit API endpoints first and conclude there is no static handler.

Confirm the handler exists with one of:

# Hit the URL the storage layer claims to produce.
curl -I https://${HOST}/images/some/key.png
// Or dump the Fastify route table at boot.
console.log(fastify.printRoutes());

Why this matters beyond the immediate 404

Gotchas

fastify static-files image-storage urls debugging

Was this helpful?

Related Entries

Reproduce Railway's npm ci locally with the exact npm versionSerialize per-user check-then-write flows with pg_advisory_xact_lock(hashtext(id))Content-hash dedup that restores a soft-deleted row must re-run quota and limit checksFix "Cannot find module" with ES modules in Node.js