Skip to content

Storage Wiring

Recipes are working wiring examples with the sharp edges annotated. This page and the storage-wiring recipe served to coding agents by the MCP server share one source (@modularityjs/manifest), so they never drift apart.

Two packages compose:

  • @modularityjs/storageStorageService abstract contract + StorageModule declaring it. Methods: read(key) → StorageObject, readStream(key) → StorageReadStream, write(key, data, metadata?), writeStream(key, stream, metadata?), exists(key) → boolean, delete(key), list(prefix?) → StorageEntry[].
  • @modularityjs/storage-{memory,local,s3} — driver binding StorageService via preferences: [{ provide: StorageService, useClass: ... }]. Each driver has a forRoot(config) static for runtime config (e.g. StorageLocalConfig.basePath, StorageS3Config.bucket).

data is Buffer | string on write; StorageObject.body is always Buffer on read. String writes get UTF-8 encoded; round-trip text via (await storage.read(key)).body.toString('utf8'). read throws NotFoundException for missing keys — use exists(key) for a boolean check.

list(prefix?) returns shallow entries ({ key, size, lastModified? }). Treat keys as opaque strings, not paths; the memory driver in particular has no concept of directories. Use /-separated namespaces (users/<id>/avatar.png) for organization — list('users/') returns every key under that prefix across all drivers.

Load both the contract module and a driver module — the contract module alone leaves StorageService unprovided and boot fails:

typescript
import { Inject, Injectable } from '@modularityjs/di';
import { inversify } from '@modularityjs/di-inversify';
import { createApp, Module, ModularityModule } from '@modularityjs/modularity';
import { StorageModule, StorageService } from '@modularityjs/storage';
import { StorageMemoryModule } from '@modularityjs/storage-memory';

@Injectable()
class AvatarService {
  constructor(
    @Inject(StorageService) private readonly storage: StorageService,
  ) {}

  async save(userId: string, png: Buffer): Promise<void> {
    await this.storage.write(`avatars/${userId}.png`, png, {
      contentType: 'image/png',
    });
  }

  async load(userId: string): Promise<Buffer | undefined> {
    const key = `avatars/${userId}.png`;
    if (!(await this.storage.exists(key))) return undefined;
    const object = await this.storage.read(key);
    return object.body;
  }
}

@Module({
  name: 'avatars',
  imports: [StorageModule],
  providers: [AvatarService],
})
class AppModule {}

const app = await createApp({
  di: inversify,
  modules: [ModularityModule, StorageModule, StorageMemoryModule, AppModule],
});

Driver switch is one line. Swap StorageMemoryModule for StorageLocalModule.forRoot({ basePath: '/var/uploads' }) or StorageS3Module.forRoot({ bucket: 'my-bucket', region: 'eu-west-1' })AvatarService doesn't change.