Putnami
DocsGitHub

Licensed under FSL-1.1-MIT

Getting Started
Concepts
How To
Build A Web App
Build An Api Service
Share Code Between Projects
Configure Your App
Add Persistence
Add Authentication
Add Background Jobs
Principles
Tooling & Workspace
Workspace Overview
Cli
Jobs & Commands
SDK
Error Handling
Extensions
Typescript
Go
Python
Docker
Ci
Frameworks
Typescript
OverviewWebReact RoutingForms And ActionsStatic FilesApiErrors And ResponsesConfigurationLoggingHttp And MiddlewareDependency InjectionPlugins And LifecycleSessionsAuthPersistenceEventsStorageCachingWebsocketsTestingHealth ChecksTelemetryProto GrpcSmart Client
Go
OverviewHttpDependency InjectionPlugins And LifecycleConfigurationSecurityPersistenceErrorsEventsStorageCachingLoggingTelemetryGrpcService ClientsValidationOpenapiTesting
Platform
  1. DocsSeparator
  2. FrameworksSeparator
  3. TypescriptSeparator
  4. Static Files

Static Files

Use the statics() plugin to serve static assets like images, CSS, JavaScript, and other files with caching and compression support.

Basic setup

import { application, http, statics } from '@putnami/application';

export const app = () =>
  application()
    .use(http({ port: 3000 }))
    .use(statics());

Configuration

Plugin options

statics({
  publicFolder: 'public',   // Folder containing static files (default: 'public')
  prefix: '/',              // URL prefix for static files (optional)
  cacheMaxAge: 86400,       // Cache duration in seconds (default: 86400 / 1 day)
  compress: 1024,           // Min file size in bytes to gzip (default: 1024)
})

URL prefix

Serve static files under a specific path:

statics({
  prefix: '/assets',
  publicFolder: 'public',
})

Files in public/ will be available at /assets/:

  • public/logo.png → /assets/logo.png
  • public/css/style.css → /assets/css/style.css

Folder structure

my-app/
  public/
    favicon.ico
    robots.txt
    images/
      logo.png
      hero.jpg
    css/
      style.css
    js/
      app.js
  src/
    main.ts

Caching

Cache-Control and ETag headers

Static files are served with both Cache-Control and ETag headers:

Cache-Control: public, max-age=86400
ETag: "a1b2c3d4e5f60718"

The cacheMaxAge option controls the max-age value in seconds:

statics({
  cacheMaxAge: 604800, // 1 week
})

Conditional requests (304 Not Modified)

The plugin handles conditional requests automatically. When a browser has a cached copy of a file, it sends the stored ETag back in an If-None-Match header. If the ETag matches, the server responds with 304 Not Modified and no body, saving bandwidth.

Cache strategies

For development:

statics({
  cacheMaxAge: 0, // No caching
})

For production with versioned assets:

statics({
  cacheMaxAge: 31536000, // 1 year
})

For frequently updated files:

statics({
  cacheMaxAge: 3600, // 1 hour
})

Compression

Files larger than the compress threshold are gzipped at build time and served with the Content-Encoding: gzip header:

statics({
  compress: 1024, // Gzip files > 1KB (default)
})

Set to 0 to disable compression:

statics({
  compress: 0,
})

Content types

The plugin automatically detects content types based on file extensions:

  • .html - text/html
  • .css - text/css
  • .js - application/javascript
  • .json - application/json
  • .png - image/png
  • .jpg, .jpeg - image/jpeg
  • .gif - image/gif
  • .svg - image/svg+xml
  • .webp - image/webp
  • .woff - font/woff
  • .woff2 - font/woff2
  • .pdf - application/pdf

Multiple static folders

Serve files from multiple locations:

import { application, http, statics } from '@putnami/application';

export const app = () =>
  application()
    .use(http({ port: 3000 }))
    .use(statics({ publicFolder: 'public' }))
    .use(statics({ publicFolder: 'uploads', prefix: '/uploads' }));

Build-time assets

Configuring assets in package.json

{
  "putnami": {
    "application": {
      "assets": [
        {
          "from": "/apps/my-app/doc",
          "to": "public/docs"
        },
        {
          "from": "/LICENSE.md",
          "to": "public/LICENSE.md"
        }
      ]
    }
  }
}

Generated assets

Assets generated during build (like compiled CSS or bundled JS) are typically placed in .gen/public/:

.gen/
  public/
    css/
      main.css
    js/
      bundle.js

React integration

When using @putnami/react, static files work alongside SSR:

import { application, http, statics } from '@putnami/application';
import { react } from '@putnami/react';

export const app = () =>
  application()
    .use(http({ port: 3000 }))
    .use(react())
    .use(statics({ publicFolder: 'public' }));

Referencing static files in React

export default function Page() {
  return (
    <div>
      <img src="/images/logo.png" alt="Logo" />
      <link rel="stylesheet" href="/css/style.css" />
    </div>
  );
}

Public folder in React config

react({
  publicFolder: 'public',  // Static files folder
})

Security considerations

Prevent directory traversal

The statics plugin automatically prevents directory traversal attacks. Requests like /../../etc/passwd are blocked.

Hide sensitive files

Don't place sensitive files in the public folder:

public/           # Served to users
  images/
  css/
private/          # Not served
  secrets.json
  .env

Exclude files

Use .gitignore patterns in your public folder if needed:

public/
  .gitignore      # Contains patterns to ignore
  images/

Performance tips

Use CDN for production

In production, consider serving static files from a CDN:

const isProduction = process.env.NODE_ENV === 'production';

// In React components
const assetPrefix = isProduction ? 'https://cdn.example.com' : '';

<img src={`${assetPrefix}/images/logo.png`} />

Compress files

Enable build-time compression for large assets:

statics({
  compress: 1024, // Gzip files > 1KB
})

Image optimization

Optimize images before deployment:

# Using imagemagick
mogrify -strip -quality 85 public/images/*.jpg

# Using optipng
optipng -o7 public/images/*.png

Common patterns

Favicon

public/
  favicon.ico
  apple-touch-icon.png
  favicon-32x32.png
  favicon-16x16.png
// In layout.tsx
<head>
  <link rel="icon" href="/favicon.ico" />
  <link rel="apple-touch-icon" href="/apple-touch-icon.png" />
</head>

Robots.txt

# public/robots.txt
User-agent: *
Allow: /
Sitemap: https://example.com/sitemap.xml

Manifest for PWA

// public/manifest.json
{
  "name": "My App",
  "short_name": "App",
  "start_url": "/",
  "display": "standalone",
  "icons": [
    {
      "src": "/icons/icon-192.png",
      "sizes": "192x192",
      "type": "image/png"
    }
  ]
}

Related guides

  • Web
  • Configuration

On this page

  • Static Files
  • Basic setup
  • Configuration
  • Plugin options
  • URL prefix
  • Folder structure
  • Caching
  • Cache-Control and ETag headers
  • Conditional requests (304 Not Modified)
  • Cache strategies
  • Compression
  • Content types
  • Multiple static folders
  • Build-time assets
  • Configuring assets in package.json
  • Generated assets
  • React integration
  • Referencing static files in React
  • Public folder in React config
  • Security considerations
  • Prevent directory traversal
  • Hide sensitive files
  • Exclude files
  • Performance tips
  • Use CDN for production
  • Compress files
  • Image optimization
  • Common patterns
  • Favicon
  • Robots.txt
  • Manifest for PWA
  • Related guides