@uploadflow/nestjs
v0.1.0
Published
NestJS dynamic module for the uploadflow file-upload pipeline (decorators, interceptors, presign controller)
Downloads
24
Maintainers
Readme
@uploadflow/nestjs
NestJS dynamic module for the uploadflow file-upload pipeline: pluggable storage (Azure Blob with real SAS / AWS S3 / local / custom), magic-byte validation, presigned direct-to-storage uploads, and record-linked uploads — all behind decorators.
npm i @uploadflow/nestjs @uploadflow/core
# storage driver (optional peer of core) — install the one you use:
npm i @azure/storage-blob
# or: npm i @aws-sdk/client-s3 @aws-sdk/lib-storage @aws-sdk/s3-request-presigner
# linked uploads on Mongo (optional): npm i @uploadflow/mongoosePeer deps: @nestjs/common, @nestjs/core, @nestjs/platform-express, reflect-metadata, rxjs.
1. Register UploadModule
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { UploadModule } from '@uploadflow/nestjs';
@Module({
imports: [
UploadModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (cfg: ConfigService) => ({
storage: {
driver: cfg.get('STORAGE_PROVIDER') === 's3' ? 's3' : 'azure',
azure: {
account: cfg.get('AZURE_STORAGE_ACCOUNT_NAME'),
accountKey: cfg.get('AZURE_STORAGE_ACCOUNT_KEY'),
container: cfg.get('AZURE_STORAGE_CONTAINER_NAME'),
},
s3: {
region: cfg.get('S3_REGION'),
bucket: cfg.get('S3_BUCKET'),
accessKeyId: cfg.get('S3_AWS_ACCESS_KEY'),
secretAccessKey: cfg.get('S3_AWS_SECRET_KEY'),
},
},
validation: { maxFileSize: '50mb', magicBytes: true },
urlMode: 'signed', // 'signed' (private, short-lived URLs) | 'public'
controller: true, // mount POST /uploads/presign + /uploads/confirm
}),
}),
],
})
export class AppModule {}forRoot(options) exists for static config. The module is global by default, so decorators work
everywhere without re-importing.
2. Direct upload — @UploadFile
import { Controller, Post } from '@nestjs/common';
import { UploadFile, UploadedFileMeta } from '@uploadflow/nestjs';
import { StoredObject } from '@uploadflow/core';
@Controller('users')
export class UserController {
@Post('avatar')
@UploadFile({ field: 'avatar', allow: ['image/png', 'image/jpeg'], maxFileSize: '5mb' })
uploadAvatar(@UploadedFileMeta() file: StoredObject) {
return file; // { key, url, size, mime, originalName } — already stored
}
}Options: { field, multiple?, maxCount?, allow?, maxFileSize?, category? }. With multiple: true,
@UploadedFileMeta() returns StoredObject[].
3. Linked upload — @LinkedUpload + @WithAttachments
Auto-links a file to the record your handler returns; injects attachments back into the response. Ordering guarantees no orphan blob if anything fails.
import { Body, Controller, Delete, Get, Param, Post } from '@nestjs/common';
import { LinkedUpload, WithAttachments } from '@uploadflow/nestjs';
@Controller('media')
export class MediaController {
@Post()
@LinkedUpload({ fields: ['mediaFile'], module: 'MEDIA', recordIdFrom: (r) => r._id })
create(@Body() dto: CreateMediaDto) {
return this.service.create(dto); // resolve to a record with _id; response gets `attachments`
}
@Get(':id')
@WithAttachments({ module: 'MEDIA' })
getById(@Param('id') id: string) {
return this.service.findById(id); // response gets `attachments` with resolved URLs
}
@Delete(':id')
@LinkedUpload({ fields: ['mediaFile'], module: 'MEDIA' })
remove(@Param('id') id: string) {
return this.service.remove(id); // attachments unlinked
}
}Linked/read decorators need an attachmentStore in module options. Zero-setup with Mongoose:
import { MongooseAttachmentStore } from '@uploadflow/mongoose';
// in useFactory (inject the mongoose Connection, e.g. getConnectionToken()):
attachmentStore: MongooseAttachmentStore.forRoot({ connection, collection: 'attachments' })Or supply any class/instance implementing AttachmentStore from @uploadflow/core.
@LinkedUpload options: { fields, module?, allow?, maxFileSize?, recordIdFrom?, createdByFrom? }.
recordIdFrom defaults to r._id ?? r.id ?? r.data._id. For PUT/PATCH, pass attachmentIds in the
body to replace existing attachments.
4. Presigned upload (with controller: true)
| Route | Body | Returns |
|---|---|---|
| POST /uploads/presign | { filename, contentType, size?, category? } | { uploadUrl, key, method, headers, expiresAt } |
| POST /uploads/confirm | { key, originalName?, mimeType? } | StoredObject |
// client
const { uploadUrl, key, headers } = await api.post('/uploads/presign',
{ filename: file.name, contentType: file.type }).then(r => r.data);
await fetch(uploadUrl, { method: 'PUT', body: file, headers }); // straight to storage
const meta = await api.post('/uploads/confirm', { key }).then(r => r.data);Prefer to own the routes? Inject PresignService and call getUploadUrl / confirm.
Errors
Validation failures are UploadflowError and are auto-mapped to HTTP (400 invalid type / unverified
content, 413 too large, 404 not found).
Full docs, config reference, custom adapters, and aggregation reads: https://github.com/OWNER/uploadflow#readme
MIT
