Storage Wiring
Recipes are working wiring examples with the sharp edges annotated. This page and the
storage-wiringrecipe served to coding agents by the MCP server share one source (@modularityjs/manifest), so they never drift apart.
Two packages compose:
@modularityjs/storage—StorageServiceabstract contract +StorageModuledeclaring 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 bindingStorageServiceviapreferences: [{ provide: StorageService, useClass: ... }]. Each driver has aforRoot(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:
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.