Skip to content

Static Assets

How τjs handles static file serving in development and production.

τjs serves static assets through Fastify plugins. By default, it registers @fastify/static to serve files from dist/client/, but you can customise or disable this behavior.

When you create a τjs server without specifying static asset handling:

import { createServer } from "@taujs/server";
await createServer({
fastify,
config,
serviceRegistry,
clientRoot: "./dist/client",
});

What happens:

  1. τjs registers @fastify/static automatically
  2. Points to dist/client/ directory
  3. Serves assets under root prefix /
  4. All apps share the same static file server

Result: Assets accessible at their build paths:

/app/assets/entry-client-abc123.js
/admin/assets/entry-client-xyz789.js

Override the default static plugin:

import fastifyStatic from "@fastify/static";
import { createServer } from "@taujs/server";
import path from "node:path";
await createServer({
fastify,
config,
serviceRegistry,
registerStaticAssets: {
plugin: fastifyStatic,
options: {
root: path.join(process.cwd(), "dist", "client"),
prefix: "/static/", // Assets under /static/ instead of /
decorateReply: false,
},
},
});

When using a CDN or reverse proxy to serve assets:

await createServer({
fastify,
config,
serviceRegistry,
clientRoot: "./dist/client",
registerStaticAssets: false, // Disable static middleware
});

Use when:

  • Assets served from CDN
  • Nginx/Apache serves static files
  • Assets on separate domain

τjs still reads manifests from clientRoot for SSR, but doesn’t serve the files itself.

τjs uses a project-level public/ directory:

project/
├── public/
│ ├── favicon.ico
│ ├── robots.txt
│ └── app/
│ └── logo.svg
└── client/
├── app/
└── admin/

During build:

  • Client build: Copies public/ contents to dist/client/
  • SSR build: Sets publicDir: false (no copying)

Result after build:

dist/client/
├── favicon.ico
├── robots.txt
├── app/
│ ├── logo.svg
│ └── assets/
└── admin/
└── assets/

Namespace assets by app:

public/
├── app/
│ ├── logo.svg
│ └── favicon.ico
└── admin/
├── logo.svg
└── favicon.ico

Reference in HTML:

<!-- Customer app -->
<img src="/app/logo.svg" />
<!-- Admin app -->
<img src="/admin/logo.svg" />

After building, upload client assets:

Terminal window
npm run build:client
# Upload to S3
aws s3 sync dist/client/ s3://my-bucket/assets/ \
--cache-control "max-age=31536000,immutable" \
--exclude "*.html"
# HTML with no-cache
aws s3 sync dist/client/ s3://my-bucket/assets/ \
--cache-control "no-cache" \
--include "*.html"
await createServer({
fastify,
config,
serviceRegistry,
clientRoot: "./dist/client", // Still needed for manifests
registerStaticAssets: false, // CDN serves assets
});
Hashed assets: max-age=31536000, immutable
HTML files: no-cache
Other files: no-cache (unless hashed)
upstream nodejs {
server localhost:3000;
}
server {
listen 80;
# Static assets - Nginx serves directly
location ~ ^/.+/assets/ {
root /app/dist/client;
add_header Cache-Control "public, max-age=31536000, immutable";
}
# Try static first, then proxy to Node
location / {
root /app/dist/client;
try_files $uri @nodejs;
}
location @nodejs {
proxy_pass http://nodejs;
proxy_set_header Host $host;
}
}

Configure τjs:

// Nginx handles static files
await createServer({
fastify,
config,
serviceRegistry,
registerStaticAssets: false,
});

Check static middleware is registered:

// Verify in logs
[τjs] Static assets: enabled

Verify files exist:

Terminal window
ls dist/client/app/assets/
# Should show built assets

Fastify detects MIME types automatically. If incorrect:

registerStaticAssets: {
plugin: fastifyStatic,
options: {
root: './dist/client',
setHeaders: (res, path) => {
if (path.endsWith('.js')) {
res.setHeader('Content-Type', 'application/javascript');
}
}
}
}