Preventing 404s for Angular Universal assets with Fastify

Summary

Replacing Express with Fastify for Angular Universal SSR can improve throughput, but a mis‑configured static file plugin often leads to 404s for assets such as JavaScript bundles, CSS, and the favicon. The root cause is typically the ordering of routes and the use of a catch‑all setNotFoundHandler that swallows static requests before the static plugin gets a chance to serve them.

Root Cause

  • Static plugin registered after a wildcard handler – Fastify evaluates routes in registration order.
  • The setNotFoundHandler intercepts every request that doesn’t match a previously registered route, so the static middleware never runs.
  • Prefix handling (prefix: "/") combined with a missing wildcard: true option can also prevent static file matching.

Why This Happens in Real Systems

  • Developers migrate from Express to Fastify and copy‑paste the setNotFoundHandler pattern without adjusting route precedence.
  • Fastify’s declarative plugin system differs from Express’ middleware stack; ordering is critical.
  • Lack of explicit wildcard/static route configuration leads to silent failures that only surface as 404s.

Real-World Impact

  • Users see a blank page or broken UI because core bundles fail to load.
  • SEO indexing drops when crawlers cannot retrieve assets.
  • Performance gains vanish; the server spends time handling 404s instead of serving cached static files.
  • Developer frustration increases as the SSR response appears to work, but the page is missing styles and scripts.

Example or Code (if necessary and relevant)

import Fastify from "fastify";
import fastifyStatic from "@fastify/static";
import { AngularAppEngine } from "@angular/ssr";
import path from "node:path";
import { fileURLToPath } from "node:url";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const browserDist = path.join(__dirname, "../dist/anexiouslink/browser");

export async function startFastifyPage() {
  const fastify = Fastify({ logger: true });

  // Register static files **before** the SSR catch‑all
  await fastify.register(fastifyStatic, {
    root: browserDist,
    prefix: "/",          // serve at root
    // optional: wildcard: true // ensures sub‑paths are matched
  });

  const angularApp = new AngularAppEngine({
    allowedHosts: ["localhost", "127.0.0.1"],
  });

  // SSR fallback – only runs when static plugin does NOT handle the request
  fastify.setNotFoundHandler(async (request, reply) => {
    const url = `${request.protocol}://${request.hostname}${request.url}`;
    const req = new Request(url, {
      method: request.method,
      headers: new Headers(request.headers as any),
      body:
        request.method !== "GET" && request.method !== "HEAD"
          ? JSON.stringify(request.body)
          : undefined,
    });

    const res = await angularApp.handle(req);
    if (!res) return reply.code(404).send("Not Found");

    reply.code(res.status);
    res.headers.forEach((value, key) => reply.header(key, value));
    const body = await res.text();
    return reply.send(body);
  });

  try {
    await fastify.listen({ port: 8888, host: "0.0.0.0" });
    fastify.log.info("SSR server running on http://localhost:8888");
  } catch (err) {
    fastify.log.error(err);
    process.exit(1);
  }
}

How Senior Engineers Fix It

  • Register static assets before any wildcard handler to guarantee route precedence.
  • Validate plugin options (prefix, wildcard, decorateReply) to match the project’s URL scheme.
  • Use Fastify’s onRequest hook for logging to confirm that static requests hit the plugin.
  • Add unit/integration tests that request known assets (/main.js, /favicon.ico) and assert a 200 response.
  • Keep the SSR fallback truly fallback: only invoke Angular when reply.raw.statusCode === 404 after the static plugin runs.

Why Juniors Miss It

  • They assume Express‑style middleware order applies to Fastify, overlooking Fastify’s plugin registration sequence.
  • They place the catch‑all handler early in the file, unintentionally shadowing static routes.
  • Lack of familiarity with Fastify’s routing diagnostics (e.g., fastify.printRoutes()) leads to missing the fact that the static route never appears.
  • Often skip testing static asset delivery during local development, catching the issue only in production.

Leave a Comment