npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@neoma/anubis

v0.1.0

Published

NestJS-idiomatic file storage — upload, persist, download — for S3-compatible backends.

Downloads

109

Readme

@neoma/anubis

NestJS-idiomatic file storage for S3-compatible backends.

Anubis (the jackal-headed Egyptian god who guarded the tombs) handles the file upload lifecycle: store to S3, persist metadata to your entity, and generate presigned download URLs -- all through interceptors and decorators that compose naturally with NestJS controllers.

Installation

npm install @neoma/anubis

Peer dependencies

npm install @nestjs/common @nestjs/core @nestjs/platform-express @nestjs/typeorm @nestjs/event-emitter typeorm reflect-metadata rxjs

Quick Start

1. Define your entity

Your entity implements the Storable interface:

import { type Storable } from "@neoma/anubis"
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm"

@Entity()
export class Upload implements Storable {
  @PrimaryGeneratedColumn("uuid")
  public id!: string

  @Column()
  public originalName!: string

  @Column()
  public mimeType!: string

  @Column()
  public size!: number

  @Column()
  public key!: string

  @Column()
  public bucket!: string

  // Add your own columns
  @Column({ nullable: true })
  public source?: string
}

2. Register the module

import { AnubisModule } from "@neoma/anubis"

@Module({
  imports: [
    TypeOrmModule.forRoot({ ... }),
    AnubisModule.forRoot({
      endpoint: "http://localhost:9000",
      region: "us-east-1",
      bucket: "uploads",
      accessKeyId: process.env.S3_ACCESS_KEY_ID,
      secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
      entity: Upload,
      maxFileSize: 10_000_000, // 10MB global limit
    }),
  ],
})
export class AppModule {}

Or with async configuration:

AnubisModule.forRootAsync({
  imports: [ConfigModule],
  useFactory: (config: ConfigService) => ({
    endpoint: config.get("S3_ENDPOINT"),
    region: config.get("S3_REGION"),
    bucket: config.get("S3_BUCKET"),
    accessKeyId: config.get("S3_ACCESS_KEY_ID"),
    secretAccessKey: config.get("S3_SECRET_ACCESS_KEY"),
    entity: Upload,
  }),
  inject: [ConfigService],
})

3. Use in your controller

import {
  Upload as UploadDecorator,
  StoredFile,
  TemporaryLink,
} from "@neoma/anubis"

@Controller("uploads")
export class UploadController {
  public constructor(private readonly dataSource: DataSource) {}

  @Post()
  @HttpCode(HttpStatus.CREATED)
  @UploadDecorator()
  public create(@StoredFile() file: Upload): Upload {
    file.source = "form"
    return file
  }

  @Post("csv")
  @HttpCode(HttpStatus.CREATED)
  @UploadDecorator({ types: ["text/csv"], maxSize: 1_000_000 })
  public importCsv(@StoredFile() file: Upload): Upload {
    file.source = "csv-import"
    return file
  }

  @Get(":id")
  @TemporaryLink()
  public async download(@Param("id") id: string): Promise<Upload> {
    return this.dataSource.getRepository(Upload).findOneByOrFail({ id })
  }
}

How it works

Upload (@Upload()):

  1. Middleware: Multer parses the multipart request (memory storage, single file)
  2. Pre-handler: Interceptor validates the file (size, type), uploads to S3, creates the entity, attaches it to the request
  3. Your handler: Receives the entity via @StoredFile(), mutates any additional fields, returns the HTTP response
  4. Post-handler: Interceptor persists the entity to the database, emits FileCreatedEvent

Download (@TemporaryLink()):

  1. Your handler: Returns a Storable entity (e.g. from database lookup)
  2. Post-handler: Interceptor generates a presigned S3 URL and responds with HTTP 302 redirect

Events

After a successful upload and entity persistence, a FileCreatedEvent is emitted:

import { FileCreatedEvent } from "@neoma/anubis"
import { OnEvent } from "@nestjs/event-emitter"

@Injectable()
export class UploadProcessor {
  @OnEvent(FileCreatedEvent.EVENT_NAME)
  public handleFileCreated(event: FileCreatedEvent<Upload>): void {
    console.log("File created:", event.entity.key)
  }
}

Events are fire-and-forget -- listener errors do not affect the upload response.

Exceptions

All exceptions extend HttpException and include metadata for diagnostics:

| Exception | Status | Properties | |-----------|--------|------------| | NoFileProvidedException | 400 | message | | FileTooLargeException | 413 | fileSize: number \| null, maxSize: number | | UnsupportedFileTypeException | 415 | mimeType: string, allowedTypes: string[] | | FileStoreUnreachableException | 503 | endpoint: string, bucket: string, cause: string |

FileTooLargeException.fileSize is null when multer rejects the file before buffering (actual size unknown).

API Reference

AnubisOptions

| Option | Type | Required | Default | Description | |--------|------|----------|---------|-------------| | endpoint | string | Yes | | S3-compatible endpoint URL | | region | string | Yes | | AWS region | | bucket | string | Yes | | S3 bucket name | | accessKeyId | string | Yes | | AWS access key ID | | secretAccessKey | string | Yes | | AWS secret access key | | entity | new () => T | Yes | | TypeORM entity class implementing Storable | | prefix | string | No | | Key prefix for uploaded files | | maxFileSize | number | No | | Global maximum file size in bytes | | allowedMimeTypes | string[] | No | | Global allowed MIME types | | linkExpiresIn | number | No | 3600 | Default presigned URL expiry in seconds | | forcePathStyle | boolean | No | true | Use path-style S3 URLs (required for MinIO) |

Storable interface

The contract your entity must implement:

interface Storable {
  id: any
  originalName: string
  mimeType: string
  size: number
  key: string
  bucket: string
}

@Upload(options?)

Method decorator for upload routes. Global limits are the ceiling; per-route narrows within it.

@Upload()                                      // defaults: field "file", no per-route limits
@Upload({ field: "avatar" })                   // custom field name
@Upload({ maxSize: 1_000_000 })                // per-route size limit (1MB)
@Upload({ types: ["text/csv"] })               // per-route type restriction
@Upload({ field: "doc", maxSize: 5_000_000 })  // combined

@StoredFile()

Parameter decorator that extracts the stored file entity from the request.

@TemporaryLink(options?)

Method decorator for download routes. Handler must return a Storable entity.

@TemporaryLink()                  // default expiry from AnubisOptions.linkExpiresIn
@TemporaryLink({ expiresIn: 600 }) // 10 minute expiry

FileCreatedEvent

Emitted after successful upload + persistence. EVENT_NAME = "anubis.file.created".

StorageService

Injected via DI. Wraps @aws-sdk/client-s3.

  • store(originalName, buffer, contentType): Promise<string> -- Upload a file, returns the generated key
  • getSignedUrl(key, expiresIn?): Promise<string> -- Generate a presigned download URL

Storage Key Format

Keys are generated as ${prefix}/${ulid}-${originalName} (or ${ulid}-${originalName} without a prefix). ULIDs provide time-ordered uniqueness. Filenames are sanitised to strip directory components.

License

MIT