npm package: @nodelabs/nest-aws-s3
Description: NestJS module for AWS S3
Author: Abiral Neupane
Latest version: 0.0.2

# @nodelabs/nest-aws-s3

A NestJS dynamic module for interacting with **AWS S3** — presigned upload URLs, signed read URLs, URL validation, and raw command execution. Supports both static (`forRoot`) and async (`forRootAsync`) configuration, with optional global registration.

---

## Table of Contents

- [Installation](#installation)
- [Quick Start](#quick-start)
- [Configuration](#configuration)
  - [forRoot (static)](#forroot-static)
  - [forRootAsync (factory)](#forrootasync-factory)
  - [isGlobal](#isglobal)
- [Available Services](#available-services)
  - [AwsS3Service](#awss3service)
- [DTOs](#dtos)
- [API Reference](#api-reference)
  - [IS3ModuleOptions](#is3moduleoptions)
  - [IS3ModuleAsyncOptions](#is3moduleasyncoptions)
- [Exports](#exports)

---

## Installation

```bash
npm install @nodelabs/nest-aws-s3 @aws-sdk/client-s3 @aws-sdk/s3-request-presigner
```

### Peer Dependencies

```bash
npm install @nestjs/common @nestjs/config reflect-metadata rxjs
```

> **Optional peers:** `@nestjs/swagger` and `class-validator` are needed only if you use the exported DTOs with Swagger or validation decorators.

---

## Quick Start

```typescript
// app.module.ts
import { Module } from '@nestjs/common';
import { S3Module } from '@nodelabs/nest-aws-s3';

@Module({
  imports: [
    S3Module.forRoot({
      region: 'us-east-1',
      accessKeyId: 'YOUR_ACCESS_KEY_ID',
      secretAccessKey: 'YOUR_SECRET_ACCESS_KEY',
      bucket: 'my-app-bucket',
    }),
  ],
})
export class AppModule {}
```

---

## Configuration

### `forRoot` (static)

```typescript
S3Module.forRoot({
  region: 'us-east-1',
  accessKeyId: 'YOUR_ACCESS_KEY_ID',
  secretAccessKey: 'YOUR_SECRET_ACCESS_KEY',
  bucket: 'my-app-bucket',
  isGlobal: true, // optional
})
```

---

### `forRootAsync` (factory)

Use `forRootAsync` to inject configuration from `ConfigService` or any other provider.

```typescript
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { S3Module } from '@nodelabs/nest-aws-s3';

@Module({
  imports: [
    ConfigModule.forRoot(),
    S3Module.forRootAsync({
      isGlobal: true,
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: (config: ConfigService) => ({
        region: config.get<string>('AWS_REGION'),
        accessKeyId: config.get<string>('AWS_ACCESS_KEY_ID'),
        secretAccessKey: config.get<string>('AWS_SECRET_ACCESS_KEY'),
        bucket: config.get<string>('AWS_S3_BUCKET'),
      }),
    }),
  ],
})
export class AppModule {}
```

#### Environment variables (example `.env`)

```env
AWS_REGION=us-east-1
AWS_ACCESS_KEY_ID=your_access_key
AWS_SECRET_ACCESS_KEY=your_secret_key
AWS_S3_BUCKET=my-app-bucket
```

---

### `isGlobal`

Setting `isGlobal: true` registers the module globally so `AwsS3Service` is available across all feature modules without re-importing `S3Module`.

---

## Available Services

### `AwsS3Service`

```typescript
import { Injectable } from '@nestjs/common';
import { AwsS3Service } from '@nodelabs/nest-aws-s3';

@Injectable()
export class FileService {
  constructor(private readonly s3Service: AwsS3Service) {}

  async getUploadUrl(fileName: string, contentType: string) {
    return this.s3Service.generateUploadUrl(fileName, contentType);
  }
}
```

#### Methods

| Method | Signature | Description |
|--------|-----------|-------------|
| `generateUploadUrl` | `generateUploadUrl(key: string, contentType: string): Promise<string>` | Generates a presigned **PUT** URL for direct browser-to-S3 uploads. |
| `generateSignedReadUrl` | `generateSignedReadUrl(key: string, expiresIn?: number): Promise<string>` | Generates a presigned **GET** URL for temporary object access. Default expiry is **3600 seconds** (1 hour). |
| `isS3Url` | `isS3Url(url: string): boolean` | Returns `true` if the given URL points to the configured S3 bucket. |
| `sanitize` | `sanitize(filePath: string): string` | Strips the S3 bucket hostname prefix from a URL, returning the object key only. |
| `execute` | `execute(command: S3Command): Promise<any \| null>` | Executes a raw `@aws-sdk/client-s3` command. Errors are logged and `null` is returned on failure. |

##### `generateUploadUrl` example

```typescript
const uploadUrl = await s3Service.generateUploadUrl(
  'avatars/user-123.png',
  'image/png',
);
// PUT the file directly to `uploadUrl` from the client
```

##### `generateSignedReadUrl` example

```typescript
// Default 1-hour expiry
const readUrl = await s3Service.generateSignedReadUrl('avatars/user-123.png');

// Custom expiry (15 minutes)
const shortUrl = await s3Service.generateSignedReadUrl('avatars/user-123.png', 900);
```

##### `isS3Url` / `sanitize` example

```typescript
const url = 'https://my-app-bucket.s3.amazonaws.com/avatars/user-123.png';

s3Service.isS3Url(url);     // true
s3Service.sanitize(url);    // 'avatars/user-123.png'
```

##### `execute` (raw command) example

```typescript
import { HeadObjectCommand } from '@aws-sdk/client-s3';

const meta = await s3Service.execute(
  new HeadObjectCommand({ Bucket: 'my-app-bucket', Key: 'avatars/user-123.png' }),
);
```

---

## DTOs

The package exports two DTOs that can be used in your NestJS controllers:

### `S3UploadRequestDto`

| Property | Type | Description |
|----------|------|-------------|
| `fileName` | `string` | Original file name requested by the client |
| `fileType` | `string` | MIME type of the file (e.g. `image/png`) |

### `S3UploadResponseDto`

| Property | Type | Description |
|----------|------|-------------|
| `url` | `string` | Presigned upload URL |
| `requestedName` | `string` | The file name that was requested |
| `acceptedType` | `string` | The MIME type that was accepted |

```typescript
import { Body, Controller, Post } from '@nestjs/common';
import { AwsS3Service, S3UploadRequestDto } from '@nodelabs/nest-aws-s3';

@Controller('files')
export class FilesController {
  constructor(private readonly s3Service: AwsS3Service) {}

  @Post('upload-url')
  async getUploadUrl(@Body() dto: S3UploadRequestDto) {
    const url = await this.s3Service.generateUploadUrl(dto.fileName, dto.fileType);
    return { url, requestedName: dto.fileName, acceptedType: dto.fileType };
  }
}
```

---

## API Reference

### `IS3ModuleOptions`

```typescript
interface IS3ModuleOptions {
  /** AWS region (e.g. 'us-east-1') */
  region: string;

  /** IAM Access Key ID */
  accessKeyId: string;

  /** IAM Secret Access Key */
  secretAccessKey: string;

  /** Target S3 bucket name */
  bucket: string;
}
```

### `IS3ModuleAsyncOptions`

```typescript
interface IS3ModuleAsyncOptions {
  /** Register the module globally */
  isGlobal?: boolean;

  /** NestJS modules to import before the factory runs */
  imports?: any[];

  /** Providers to inject into useFactory */
  inject?: any[];

  /** Factory function that returns IS3ModuleOptions */
  useFactory: (...args: any[]) => IS3ModuleOptions | Promise<IS3ModuleOptions>;
}
```

---

## Exports

| Symbol | Description |
|--------|-------------|
| `S3Module` | Dynamic NestJS module (`forRoot` / `forRootAsync`) |
| `AwsS3Service` | Injectable S3 service |
| `S3_MODULE_OPTIONS` | Injection token for options object |
| `IS3ModuleOptions` | TypeScript interface for module options |
| `IS3ModuleAsyncOptions` | TypeScript interface for async options |
| `S3UploadRequestDto` | DTO for upload-URL request payload |
| `S3UploadResponseDto` | DTO for upload-URL response payload |

---

## License

MIT