Skip to content

Static Asset Component ​

StaticAssetComponent reads a map of storage backend configurations and generates a full bucket/object REST controller for each one - disk or S3-compatible - both through the same IStorageHelper contract.

IMPORTANT

StaticAssetComponent and its related exports are not on the @venizia/ignis root barrel. Import from the @venizia/ignis/static-asset subpath.

In one example ​

typescript
import { BaseApplication } from '@venizia/ignis';
import {
  StaticAssetComponent,
  StaticAssetComponentBindingKeys,
  StaticAssetStorageTypes,
} from '@venizia/ignis/static-asset';
import type { TStaticAssetsComponentOptions } from '@venizia/ignis/static-asset';
import { DiskHelper } from '@venizia/ignis-helpers';

export class Application extends BaseApplication {
  preConfigure() {
    this.bind<TStaticAssetsComponentOptions>({
      key: StaticAssetComponentBindingKeys.STATIC_ASSET_COMPONENT_OPTIONS,
    }).toValue({
      staticAsset: {
        controller: { name: 'AssetController', basePath: '/assets', isStrict: true },
        storage: StaticAssetStorageTypes.DISK,
        helper: new DiskHelper({ basePath: './app_data/assets' }),
        extra: { parseMultipartBody: { storage: 'memory' } },
      },
    });

    this.component(StaticAssetComponent);
  }
}

This registers GET/POST/DELETE on /assets/buckets/:bucketName, POST /assets/buckets/:bucketName/objects, GET /assets/buckets/:bucketName/objects, plus stream/download/delete-object routes - no controller class to write by hand.

How it works ​

  • One options key, one generated controller. TStaticAssetsComponentOptions is a record. Each key (staticAsset above) becomes an independently configured storage backend with its own basePath. AssetControllerFactory.defineAssetController() builds the controller inside StaticAssetComponent.binding().

  • storage and helper are a discriminated pair. Mismatching them fails at compile time, not at runtime:

    storageRequired helper
    'disk'DiskHelper
    'bun-s3'BunS3Helper
    'minio' (deprecated)MinioHelper

    StaticAssetStorageTypes.MINIO is still a live arm of the union, marked @deprecated. Use StaticAssetStorageTypes.BUN_S3 with a BunS3Helper instead - it reaches MinIO over the same S3 API.

  • Every backend implements the same IStorageHelper contract. DiskHelper, BunS3Helper and MinioHelper all extend BaseStorageHelper. Bucket/object operations, name validation (isValidBucketName/isValidObjectKey), and upload normalization behave identically regardless of backend.

  • Object names can embed folder paths, encoded as one segment. objects/{objectName} percent-encodes the whole folder/file.ext string via encodeURIComponent(), which also escapes /. Hono decodes it before your handler runs - encode the name client-side, and never decode it again.

  • MetaLink is opt-in. Set useMetaLink: true and provide metaLink.repository. IGNIS then persists a database row (bucket/object/mimetype/size/etag/principal/variant) alongside every upload. That row lets you query "which files does user X own" without listing a whole bucket.

  • The default binding is empty. StaticAssetComponentBindingKeys.STATIC_ASSET_COMPONENT_OPTIONS defaults to {}. Bind it with at least one storage backend before this.component(StaticAssetComponent) - an empty binding produces zero routes.

Common tasks ​

Add a second storage backend ​

Each key in the options object is independent - mix disk and S3 under different base paths in one binding.

typescript
import { BunS3Helper } from '@venizia/ignis-helpers/bun-s3';

this.bind<TStaticAssetsComponentOptions>({
  key: StaticAssetComponentBindingKeys.STATIC_ASSET_COMPONENT_OPTIONS,
}).toValue({
  uploads: {
    controller: { name: 'UploadsController', basePath: '/uploads' },
    storage: StaticAssetStorageTypes.BUN_S3,
    helper: new BunS3Helper({
      endpoint: { default: 'http://localhost:9000' },
      accessKey: process.env.S3_ACCESS_KEY,
      secretKey: process.env.S3_SECRET_KEY,
    }),
  },
  tempFiles: {
    controller: { name: 'TempController', basePath: '/temp' },
    storage: StaticAssetStorageTypes.DISK,
    helper: new DiskHelper({ basePath: './temp' }),
  },
});

Lock down individual routes ​

Each key under controller.routes accepts a partial authenticate/authorize/middleware/path override, plus enabled to drop the route entirely; unset routes stay public and registered.

typescript
{
  controller: {
    name: 'AssetController',
    basePath: '/assets',
    routes: {
      upload: { authenticate: { strategies: ['jwt'], mode: 'required' } },
      deleteObject: { authenticate: { strategies: ['jwt'], mode: 'required' } },
      deleteBucket: { enabled: false },
    },
  },
}

Split a shared bucket between controllers ​

controller.keyPrefix scopes every key a controller touches: a key outside the prefix answers 404 before any storage call, listObjects never lists outside it, and an upload key outside it is refused. See Share one bucket between controllers for the full example.

Switch multipart parsing to disk for large uploads ​

parseMultipartBody: { storage: 'memory' } (the default) buffers the whole file in RAM before writing it to the backend. Switch to 'disk' to spool it to a temp file instead.

typescript
extra: {
  parseMultipartBody: { storage: 'disk', uploadDir: './tmp/uploads' },
}

Bind a repository over BaseMetaLinkModel (used directly, no subclass needed) and pass it as metaLink.repository.

typescript
useMetaLink: true,
metaLink: {
  model: BaseMetaLinkModel,
  repository: this.get<MetaLinkRepository>({ key: 'repositories.MetaLinkRepository' }),
},

See Enable MetaLink tracking for the full repository/table/binding setup.

normalizeNameFn runs before the file is written to storage; normalizeLinkFn builds the link value returned to the client. Both are optional - omit either to keep the component's defaults.

Both take one options object. normalizeNameFn receives { file }, and normalizeLinkFn receives an IObjectLocation - { bucket: { name }, object: { key } }.

typescript
extra: {
  normalizeNameFn: ({ file }) =>
    `${Date.now()}_${file.originalName.toLowerCase().replace(/\s/g, '_')}`,
  normalizeLinkFn: ({ bucket, object }) =>
    `/api/files/${bucket.name}/${encodeURIComponent(object.key)}`,
},

See also ​

Files: