Factory drive module for NestJS framework
nestjs_module_factorydrive provides a simple storage abstraction for NestJS:
- configure one or many disks
- select a default disk
- use built-in local filesystem driver
- declare custom drivers (S3, Spaces, etc.) directly in
forRoot()/forRootAsync()
Current maintained packages in the Factorydrive ecosystem:
local:nestjs_module_factorydrives3:nestjs_module_factorydrive-s3sftp:nestjs_module_factorydrive-sftpmcp:nestjs_module_factorydrive-mcp
- Node.js
>= 22 - Yarn
1.22.22(used for development in this repository) - NestJS
^6to^12(@nestjs/commonand@nestjs/core) - With NestJS 12 (ESM-only), Node.js
>= 22.12so the CommonJS build canrequire()Nest
Application and business services should depend on FactorydriveService, not on a
physical storage provider. Keep filesystem roots, buckets, endpoints, credentials, and
driver selection in module configuration so the same business code can use local, S3,
or SFTP storage.
Business service
|
FactorydriveService
|
AbstractStorage
/ | \
local S3 SFTP
For application storage, avoid importing node:fs, S3Client, or an SFTP client into
business services. Provider-specific SDKs belong inside Factorydrive drivers or narrowly
justified infrastructure code.
yarn add @ficsysfr/nestjs_module_factorydriveThis repository uses Yarn, Vitest, TypeScript, and Biome:
yarn install --frozen-lockfile
yarn lint
yarn typecheck
yarn test
yarn test:coverage
yarn build
yarn mcp:test
yarn docs:build
yarn docs:check
yarn test:scripts
yarn changelog:check
yarn package:checkThe equivalent aggregate command is make check. Build audited core and MCP tarballs
with yarn package or make package; outputs and SHA256SUMS.txt are written under
.artifacts/npm/.
Maintainers dispatch an exact manual release with
make release VERSION=2.0.0 CHANNEL=latest WATCH=1. The privileged workflow uses the
protected npm environment and npm Trusted Publishing/OIDC after the one-time bootstrap.
// app.module.ts
import { Module } from '@nestjs/common'
import { FactorydriveModule } from '@ficsysfr/nestjs_module_factorydrive'
@Module({
imports: [
FactorydriveModule.forRoot({
default: 'local',
disks: {
local: {
driver: 'local',
config: {
root: `${process.cwd()}/storage`,
},
},
},
}),
],
})
export class AppModule {}// app.module.ts
import { Module } from '@nestjs/common'
import { ConfigModule, ConfigService } from '@nestjs/config'
import { FactorydriveModule } from '@ficsysfr/nestjs_module_factorydrive'
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
FactorydriveModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: async (config: ConfigService) => ({
default: config.get<string>('factorydrive.default', 'local'),
disks: {
local: {
driver: 'local',
config: {
root: config.get<string>('factorydrive.localRoot', `${process.cwd()}/storage`),
},
},
},
}),
}),
],
})
export class AppModule {}Inject FactorydriveService and interact with a disk instance:
// file-storage.service.ts
import { Injectable } from '@nestjs/common'
import { FactorydriveService } from '@ficsysfr/nestjs_module_factorydrive'
@Injectable()
export class FileStorageService {
public constructor(private readonly factorydrive: FactorydriveService) {}
public async uploadFile(path: string, buffer: Buffer): Promise<void> {
await this.factorydrive.getDisk().put(path, buffer)
}
public async readFile(path: string): Promise<string> {
const { content } = await this.factorydrive.getDisk().get(path)
return content
}
public async deleteFile(path: string): Promise<boolean | null> {
const { wasDeleted } = await this.factorydrive.getDisk().delete(path)
return wasDeleted
}
}If no disk name is provided, the configured default disk is used. Prefer this form so
business code remains portable across storage providers:
const disk = this.factorydrive.getDisk()Select a named disk only when the use case intentionally targets it:
const archive = this.factorydrive.getDisk('archive')The package includes a local driver with the following operations:
append(location, content)copy(src, dest)delete(location)exists(location)get(location, encoding?)getBuffer(location)getStat(location)getStream(location)move(src, dest)prepend(location, content)put(location, content)flatList(prefix?)getUrl(location)— unsigned URL built from the diskbaseUrlgetSignedUrl(location, { expiresIn? })— time-limited HMAC-signed URL (defaultexpiresIn = 900)verifySignedUrl(location, { expires, signature })— constant-time signature + expiry check
content for put accepts Buffer | ReadableStream | string.
The local driver has no HTTP server: getSignedUrl returns a URL pointing at a baseUrl endpoint
that you expose and which must call verifySignedUrl before streaming the file. Configure the
disk with a signatureSecret and a baseUrl:
disks: {
local: {
driver: 'local',
config: {
root: '/var/data',
signatureSecret: process.env.STORAGE_URL_SECRET,
baseUrl: 'https://api.example.com/files',
},
},
}
const { signedUrl } = await storage.getSignedUrl('threads/abc', { expiresIn: 3600 })
// -> https://api.example.com/files/threads/abc?expires=...&signature=...
// in the /files endpoint:
const ok = storage.verifySignedUrl('threads/abc', { expires, signature })Both signatureSecret and baseUrl are required for signing; otherwise getSignedUrl throws
InvalidConfigException.
Custom drivers must extend AbstractStorage and implement the methods you need. A
driver's constructor takes the disk's config as its single parameter, which is what
lets it be typed with the exported StorageDriverConstructor<TConfig> helper.
// aws-s3.storage.ts
import { AbstractStorage, DeleteResponse, Response } from '@ficsysfr/nestjs_module_factorydrive'
export interface AwsS3StorageConfig {
bucket: string
}
export class AwsS3Storage extends AbstractStorage {
public constructor(private readonly config: AwsS3StorageConfig) {
super()
}
public async put(location: string, content: Buffer | NodeJS.ReadableStream | string): Promise<Response> {
// Upload implementation...
return { raw: { location, uploaded: true, contentType: typeof content } }
}
public async delete(location: string): Promise<DeleteResponse> {
// Delete implementation...
return { raw: { location }, wasDeleted: true }
}
}Then declare it directly in forRoot(), alongside the disks that use it — no module
constructor required just to register a driver:
// app.module.ts
import { Module } from '@nestjs/common'
import { FactorydriveModule } from '@ficsysfr/nestjs_module_factorydrive'
import { AwsS3Storage } from './aws-s3.storage'
@Module({
imports: [
FactorydriveModule.forRoot({
default: 's3',
drivers: {
s3: AwsS3Storage,
},
disks: {
s3: {
driver: 's3',
config: {
bucket: 'example',
},
},
},
}),
],
})
export class AppModule {}forRootAsync() accepts drivers the same way, from useFactory or useClass:
// app.module.ts
import { Module } from '@nestjs/common'
import { ConfigModule, ConfigService } from '@nestjs/config'
import { FactorydriveModule } from '@ficsysfr/nestjs_module_factorydrive'
import { AwsS3Storage } from './aws-s3.storage'
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
FactorydriveModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
default: 'assets',
drivers: { s3: AwsS3Storage },
disks: {
assets: {
driver: 's3',
config: { bucket: config.getOrThrow<string>('S3_BUCKET') },
},
},
}),
}),
],
})
export class AppModule {}The @ficsysfr/nestjs_module_factorydrive-s3 driver's configuration extends AWS SDK v3
S3ClientConfig. Any S3-compatible endpoint works the same way — Factorydrive has no
provider-specific code path, RustFS included:
FactorydriveModule.forRoot({
default: 'assets',
drivers: {
s3: AwsS3Storage,
},
disks: {
assets: {
driver: 's3',
config: {
bucket: 'my-assets',
endpoint: 'http://rustfs:9000',
region: 'us-east-1',
forcePathStyle: true,
credentials: {
accessKeyId: process.env.S3_ACCESS_KEY!,
secretAccessKey: process.env.S3_SECRET_KEY!,
},
},
},
},
})FactorydriveService.registerDriver('key', DriverClass) remains available for dynamic
registration and for existing applications: it does not need to change to keep working.
export class AppModule {
public constructor(factorydrive: FactorydriveService) {
factorydrive.registerDriver('s3', AwsS3Storage)
}
}Registering the same key with the same class through drivers and/or
registerDriver() is a no-op, so a driver can be migrated to drivers one at a time
without breaking a registerDriver() call left behind elsewhere. Registering the same
key with two different classes throws InvalidConfigException as soon as either
registration came from drivers; two conflicting registerDriver() calls (including
one that replaces the built-in local driver) keep the pre-2.1 behavior — the last call
wins — but now log a warning instead of replacing silently.
Main exports from this package:
FactorydriveModuleFactorydriveServiceAbstractStorageStorageManagerStorageDriverConstructor— the driver constructor type used bydrivers- storage config/types from
factorydrive/types - exceptions from
exceptions
The module provides dedicated exceptions (for example):
InvalidConfigException— invaliddriversentry, or a driver name registered twice with two different classesDriverNotSupportedException— a disk'sdriverwas never registered; the message names both the driver and the disk, e.g.Factorydrive driver "s3" required by disk "assets" is not registered. Declare it in "drivers" or call registerDriver() before module initialization.FileNotFoundExceptionPermissionMissingExceptionMethodNotSupportedException
Catch and map them in your service/controller layers as needed.
The npm package includes an Agent Skills pack at
node_modules/@ficsysfr/nestjs_module_factorydrive/agent-skills/public. Installing the
package with Yarn makes these two skill directories available to compatible coding
agents; the package does not run an installer or copy skills during installation.
The English use-factorydrive skill
teaches agents to explain, configure, audit, and implement Factorydrive without
coupling business code to a storage provider.
Example prompts:
Use $use-factorydrive to configure local document storage in this NestJS application.
Use $use-factorydrive to migrate this service from S3Client to FactorydriveService.
Use $use-factorydrive to expose a verified local signed-download endpoint.
Driver authors should use the separate
factorydrive-driver skill.
- Documentation: https://ficsysfr.github.io/nestjs_module_factorydrive/
- Machine-readable index: https://ficsysfr.github.io/nestjs_module_factorydrive/llms.txt
- Full context bundle: https://ficsysfr.github.io/nestjs_module_factorydrive/llms-full.txt
- MCP server:
npx -y @ficsysfr/nestjs_module_factorydrive-mcp
The documentation MCP exposes list_doc_sources, search_docs, and fetch_docs
over stdio. It never receives storage configuration and cannot read or mutate application files.
Version 2.0.0 moves the maintained ecosystem to the @ficsysfr npm scope without
changing exported TypeScript symbols. Replace package names and import specifiers, then
upgrade the core and every installed driver together. See the
migration guide.
Apache-2.0, see LICENSE. Copyright 2026 FicSys.
Versions up to and including 2.0.0 were published under the MIT License and remain available under those terms.