> ## Documentation Index
> Fetch the complete documentation index at: https://sonamu.cartanova.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# @upload

> Decorator for creating file upload APIs

The `@upload` decorator creates an API that handles file uploads. It automatically parses multipart form-data requests and provides file objects. Can be used independently without `@api` decorator, automatically setting `POST` method and multipart clients (`axios-multipart`, `tanstack-mutation-multipart`).

## Upload Modes

Sonamu provides two file upload modes:

| Mode                 | Option                      | Context Property | File Type        | Features                                                         |
| -------------------- | --------------------------- | ---------------- | ---------------- | ---------------------------------------------------------------- |
| **Buffer** (default) | `consume: "buffer"` or omit | `bufferedFiles`  | `BufferedFile[]` | Loads into memory, flexible for MD5 calculation/image processing |
| **Stream**           | `consume: "stream"`         | `uploadedFiles`  | `UploadedFile[]` | Streams directly to storage, suitable for large files            |

## Buffer Mode (Default)

Buffer mode loads files into memory before processing. Use this when you need to work directly with file contents like MD5 hash calculation or image resizing.

```typescript theme={null}
import { BaseModelClass, upload, Sonamu } from "sonamu";

class FileModelClass extends BaseModelClass {
  @upload() // default: consume: "buffer"
  async uploadAvatar() {
    const { bufferedFiles } = Sonamu.getContext();
    const file = bufferedFiles?.[0];

    if (!file) {
      throw new Error("No file uploaded");
    }

    // Use MD5 hash to prevent duplicates
    const md5 = await file.md5();
    const key = `avatars/${md5}.${file.extname}`;
    const url = await file.saveToDisk("fs", key);

    return { url, filename: file.filename, size: file.size };
  }
}
```

### BufferedFile Object

```typescript theme={null}
class BufferedFile {
  // Original file name
  get filename(): string;

  // MIME type
  get mimetype(): string;

  // File size (bytes)
  get size(): number;

  // Extension (without dot, e.g., "jpg", "png")
  get extname(): string | false;

  // File Buffer (data loaded in memory)
  get buffer(): Buffer;

  // URL after saveToDisk (Unsigned)
  get url(): string;

  // Signed URL after saveToDisk
  get signedUrl(): string;

  // Access to raw Fastify MultipartFile
  get raw(): MultipartFile;

  // Calculate MD5 hash
  async md5(): Promise<string>;

  // Save file to disk (returns URL)
  async saveToDisk(diskName: DriverKey, key: string): Promise<string>;
}
```

<Warning>The parameter order for `saveToDisk` is `(diskName, key)`.</Warning>

## Stream Mode

Stream mode streams files directly to storage without loading them into memory. Suitable for large file uploads.

```typescript theme={null}
@upload({
  consume: "stream",
  destination: "s3",  // disk name to save to
  keyGenerator: (file) => `uploads/${Date.now()}-${file.filename}`,  // key generator function
  limits: { files: 5 }
})
async uploadLargeFiles() {
  const { uploadedFiles } = Sonamu.getContext();

  if (!uploadedFiles || uploadedFiles.length === 0) {
    throw new Error("No files uploaded");
  }

  // Files are already uploaded to storage
  return {
    files: uploadedFiles.map((file) => ({
      filename: file.filename,
      url: file.url,
      key: file.key,
      size: file.size,
    })),
  };
}
```

### UploadedFile Object

In Stream mode, files are already uploaded to storage, so only metadata is accessible.

```typescript theme={null}
class UploadedFile {
  // Original file name
  get filename(): string;

  // MIME type
  get mimetype(): string;

  // File size (bytes)
  get size(): number;

  // Extension (without dot, e.g., "jpg", "png")
  get extname(): string | false;

  // Stored URL (Unsigned)
  get url(): string;

  // Stored Signed URL
  get signedUrl(): string;

  // Key in storage
  get key(): string;

  // Disk name where file is stored
  get diskName(): DriverKey;

  // Download file from storage (for later processing)
  async download(): Promise<Buffer>;
}
```

## Options

### guards

Specifies guards to apply to the upload API. Use this for upload APIs that require authentication.

```typescript theme={null}
@upload({ guards: ["user"] })
async uploadAvatar() {
  const { user, bufferedFiles } = Sonamu.getContext();
  // With user guard applied, only authenticated users can access
  // ...
}
```

### description

Specifies the API description. Displayed in auto-generated API documentation.

```typescript theme={null}
@upload({ description: "Upload user profile image" })
async uploadAvatar() {
  // ...
}
```

### limits

Specifies file upload limits. Accepts Fastify multipart's `limits` options directly.

```typescript theme={null}
type UploadDecoratorOptions = {
  // Common options
  guards?: GuardKey[]; // Guards to apply (e.g., ["user", "admin"])
  description?: string; // API description
  limits?: {
    fileSize?: number; // Max file size (bytes)
    files?: number; // Max number of files
    fields?: number; // Max number of fields
    fieldSize?: number; // Max field size
    parts?: number; // Max number of parts
  };
} &
  // Buffer mode
  (| { consume?: "buffer" }
    // Stream mode
    | {
        consume: "stream";
        destination: DriverKey;
        keyGenerator?: (file: { filename: string; mimetype: string }) => string;
      }
  );
```

```typescript theme={null}
@upload({
  limits: {
    fileSize: 10 * 1024 * 1024, // 10MB
    files: 5,                    // Max 5 files
  }
})
async uploadWithLimits() {
  const { bufferedFiles } = Sonamu.getContext();
  // ...
}
```

<Tabs>
  <Tab title="Single File (Buffer)" icon="file">
    ```typescript theme={null}
    @upload()
    async uploadAvatar() {
      const { bufferedFiles } = Sonamu.getContext();
      const file = bufferedFiles?.[0];

      if (!file) {
        throw new Error("No file");
      }

      const md5 = await file.md5();
      return {
        filename: file.filename,
        size: file.size,
        md5
      };
    }
    ```
  </Tab>

  <Tab title="Multiple Files (Buffer)" icon="files">
    ```typescript theme={null}
    @upload({ limits: { files: 10 } })
    async uploadDocuments() {
      const { bufferedFiles } = Sonamu.getContext();

      if (!bufferedFiles || bufferedFiles.length === 0) {
        throw new Error("No files");
      }

      const results = [];
      for (const file of bufferedFiles) {
        const md5 = await file.md5();
        const key = `documents/${md5}.${file.extname}`;
        const url = await file.saveToDisk("fs", key);
        results.push({
          filename: file.filename,
          url
        });
      }

      return results;
    }
    ```
  </Tab>

  <Tab title="Stream Mode" icon="cloud-arrow-up">
    ```typescript theme={null}
    @upload({
      consume: "stream",
      destination: "s3",
      keyGenerator: (file) => `uploads/${Date.now()}-${file.filename}`,
      limits: { files: 5 }
    })
    async uploadToCloud() {
      const { uploadedFiles } = Sonamu.getContext();

      if (!uploadedFiles || uploadedFiles.length === 0) {
        throw new Error("No files");
      }

      // Files are already uploaded to S3
      return uploadedFiles.map((file) => ({
        filename: file.filename,
        url: file.url,
        key: file.key
      }));
    }
    ```
  </Tab>
</Tabs>

## Getting File Information

```typescript theme={null}
@upload()
async uploadFile() {
  const { bufferedFiles } = Sonamu.getContext();
  const file = bufferedFiles?.[0];

  if (!file) {
    throw new Error("No file");
  }

  console.log("Filename:", file.filename);
  console.log("MIME type:", file.mimetype);
  console.log("Size:", file.size);
  console.log("Extension:", file.extname);

  return { uploaded: true };
}
```

## File Processing (Buffer Mode)

### Accessing Buffer

```typescript theme={null}
@upload()
async processImage() {
  const { bufferedFiles } = Sonamu.getContext();
  const file = bufferedFiles?.[0];

  if (!file) {
    throw new Error("No file");
  }

  // Direct buffer access
  const buffer = file.buffer;

  // Image processing
  const processed = await sharp(buffer)
    .resize(300, 300)
    .toBuffer();

  return { size: processed.length };
}
```

### Saving Files

```typescript theme={null}
@upload()
async uploadDocument() {
  const { bufferedFiles } = Sonamu.getContext();
  const file = bufferedFiles?.[0];

  if (!file) {
    throw new Error("No file");
  }

  // Save to disk (diskName, key order)
  const md5 = await file.md5();
  const key = `uploads/${md5}.${file.extname}`;
  const url = await file.saveToDisk("fs", key);

  // Access saved URLs
  console.log("URL:", file.url);
  console.log("Signed URL:", file.signedUrl);

  return { url, filename: file.filename, size: file.size };
}
```

### MD5 Hash Calculation

```typescript theme={null}
@upload()
async uploadWithHash() {
  const { bufferedFiles } = Sonamu.getContext();
  const file = bufferedFiles?.[0];

  if (!file) {
    throw new Error("No file");
  }

  // Calculate MD5 hash (useful for preventing duplicate files)
  const hash = await file.md5();

  // Use hash as filename
  const key = `uploads/${hash}${file.extname ? `.${file.extname}` : ''}`;
  const url = await file.saveToDisk("fs", key);

  return { url, hash };
}
```

## Using Storage Drivers

Sonamu provides multiple storage drivers.

```typescript theme={null}
@upload()
async uploadToS3() {
  const { bufferedFiles } = Sonamu.getContext();
  const file = bufferedFiles?.[0];

  if (!file) {
    throw new Error("No file");
  }

  // Save to S3 disk (configured in sonamu.config.ts)
  const md5 = await file.md5();
  const key = `avatars/${md5}.${file.extname}`;
  const url = await file.saveToDisk("s3", key);

  return { url };
}
```

<Info>
  Storage drivers are configured in `sonamu.config.ts`. Pass the disk name as the first parameter.
</Info>

## Using with Other Decorators

### With @transactional

```typescript theme={null}
@upload()
@transactional()
async uploadAndSave() {
  const { bufferedFiles } = Sonamu.getContext();
  const file = bufferedFiles?.[0];

  if (!file) {
    throw new Error("No file");
  }

  const wdb = this.getDB("w");

  // File save + DB update in transaction
  const md5 = await file.md5();
  const key = `documents/${md5}.${file.extname}`;
  const url = await file.saveToDisk("fs", key);

  await wdb.table("documents").insert({
    filename: file.filename,
    url,
    size: file.size,
    created_at: new Date()
  });

  return { url };
}
```

## Client Usage (Web)

Sonamu automatically generates file upload client code.

### Axios (Single File)

```typescript theme={null}
import { FileService } from "@/services/FileService";

const formData = new FormData();
formData.append("file", file);

const result = await FileService.uploadAvatar(formData);
```

### Axios (Multiple Files)

```typescript theme={null}
const formData = new FormData();
files.forEach((file) => {
  formData.append("files", file);
});

const result = await FileService.uploadDocuments(formData);
```

### React Example

```typescript theme={null}
import { useState } from "react";
import { FileService } from "@/services/FileService";

function FileUploader() {
  const [uploading, setUploading] = useState(false);

  const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0];
    if (!file) return;

    setUploading(true);
    try {
      const formData = new FormData();
      formData.append("file", file);

      const result = await FileService.uploadAvatar(formData);
      console.log("Uploaded:", result.url);
    } catch (error) {
      console.error("Upload failed:", error);
    } finally {
      setUploading(false);
    }
  };

  return (
    <div>
      <input
        type="file"
        onChange={handleFileChange}
        disabled={uploading}
      />
      {uploading && <p>Uploading...</p>}
    </div>
  );
}
```

### TanStack Query Example

```typescript theme={null}
import { useMutation } from "@tanstack/react-query";
import { FileService } from "@/services/FileService";

function useUploadFile() {
  return useMutation({
    mutationFn: (file: File) => {
      const formData = new FormData();
      formData.append("file", file);
      return FileService.uploadAvatar(formData);
    },
    onSuccess: (data) => {
      console.log("Upload success:", data);
    },
    onError: (error) => {
      console.error("Upload failed:", error);
    }
  });
}

function FileUploader() {
  const upload = useUploadFile();

  const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0];
    if (file) {
      upload.mutate(file);
    }
  };

  return (
    <div>
      <input
        type="file"
        onChange={handleFileChange}
        disabled={upload.isPending}
      />
      {upload.isPending && <p>Uploading...</p>}
      {upload.isSuccess && <p>Success: {upload.data.url}</p>}
      {upload.isError && <p>Error: {upload.error.message}</p>}
    </div>
  );
}
```

## File Validation

### MIME Type Validation

```typescript theme={null}
@upload()
async uploadImage() {
  const { bufferedFiles } = Sonamu.getContext();
  const file = bufferedFiles?.[0];

  if (!file) {
    throw new Error("No file");
  }

  const allowedTypes = ["image/jpeg", "image/png", "image/gif"];
  if (!allowedTypes.includes(file.mimetype)) {
    throw new Error(`Invalid file type: ${file.mimetype}`);
  }

  return await this.processImage(file);
}
```

### File Size Validation

```typescript theme={null}
@upload()
async uploadDocument() {
  const { bufferedFiles } = Sonamu.getContext();
  const file = bufferedFiles?.[0];

  if (!file) {
    throw new Error("No file");
  }

  const maxSize = 10 * 1024 * 1024; // 10MB
  if (file.size > maxSize) {
    throw new Error("File too large");
  }

  return await this.saveDocument(file);
}
```

### File Extension Validation

```typescript theme={null}
@upload()
async uploadFile() {
  const { bufferedFiles } = Sonamu.getContext();
  const file = bufferedFiles?.[0];

  if (!file) {
    throw new Error("No file");
  }

  const allowedExtensions = ["jpg", "png", "pdf"];
  if (!file.extname || !allowedExtensions.includes(file.extname)) {
    throw new Error(`Invalid file extension: ${file.extname}`);
  }

  return await this.saveFile(file);
}
```

## Image Processing

### Using Sharp

```typescript theme={null}
import sharp from "sharp";

@upload()
async uploadAndResizeImage() {
  const { bufferedFiles } = Sonamu.getContext();
  const file = bufferedFiles?.[0];

  if (!file) {
    throw new Error("No file");
  }

  const buffer = file.buffer;

  // Resize image
  const resized = await sharp(buffer)
    .resize(800, 600, { fit: "inside" })
    .jpeg({ quality: 80 })
    .toBuffer();

  // Create thumbnail
  const thumbnail = await sharp(buffer)
    .resize(200, 200, { fit: "cover" })
    .jpeg({ quality: 70 })
    .toBuffer();

  // Upload to S3 (directly save Buffer)
  const imageKey = `images/${Date.now()}.jpg`;
  await Sonamu.storage.use("s3").put(imageKey, resized);
  const imageUrl = await Sonamu.storage.use("s3").getUrl(imageKey);

  const thumbKey = `thumbnails/${Date.now()}.jpg`;
  await Sonamu.storage.use("s3").put(thumbKey, thumbnail);
  const thumbUrl = await Sonamu.storage.use("s3").getUrl(thumbKey);

  return { imageUrl, thumbUrl };
}
```

## Constraints

### 1. Independent Usage without @api

`@upload` is used independently without `@api` decorator:

```typescript theme={null}
// Correct usage
@upload()
async uploadFile() {}

// Unnecessary - @upload automatically sets up API
@api({ httpMethod: "POST" })
@upload()
async uploadFile() {}
```

### 2. httpMethod is POST

When using `@upload`, `httpMethod: "POST"` is automatically set.

### 3. Automatic clients Setting

When using `@upload`, the `clients` option is automatically set to `["axios-multipart", "tanstack-mutation-multipart"]`:

```typescript theme={null}
@upload()
async uploadFile() {
  // clients: ["axios-multipart", "tanstack-mutation-multipart"]
}
```

## Examples

<Tabs>
  <Tab title="Profile Image" icon="user-circle">
    ```typescript theme={null}
    class UserModelClass extends BaseModelClass {
      @upload()
      @transactional()
      async uploadAvatar() {
        const { user, bufferedFiles } = Sonamu.getContext();
        const file = bufferedFiles?.[0];

        if (!file) {
          throw new Error("No file uploaded");
        }

        // Validate image type
        const allowedTypes = ["image/jpeg", "image/png", "image/webp"];
        if (!allowedTypes.includes(file.mimetype)) {
          throw new Error("Invalid image type");
        }

        // Process image
        const buffer = file.buffer;
        const processed = await sharp(buffer)
          .resize(300, 300, { fit: "cover" })
          .jpeg({ quality: 85 })
          .toBuffer();

        // Upload to S3
        const key = `avatars/${user.id}/${Date.now()}.jpg`;
        await Sonamu.storage.use("s3").put(key, processed);
        const url = await Sonamu.storage.use("s3").getUrl(key);

        // Update DB
        const wdb = this.getDB("w");
        await wdb.table("users")
          .where("id", user.id)
          .update({ avatar_url: url });

        return { url };
      }
    }
    ```
  </Tab>

  <Tab title="Multiple File Upload" icon="images">
    ```typescript theme={null}
    class DocumentModelClass extends BaseModelClass {
      @upload({ limits: { files: 10 } })
      @transactional()
      async uploadDocuments(params: { projectId: number }) {
        const { bufferedFiles } = Sonamu.getContext();
        const { projectId } = params;

        if (!bufferedFiles || bufferedFiles.length === 0) {
          throw new Error("No files uploaded");
        }

        const wdb = this.getDB("w");
        const results = [];

        for (const file of bufferedFiles) {
          // Validate file type
          const allowedTypes = [
            "application/pdf",
            "application/msword",
            "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
          ];

          if (!allowedTypes.includes(file.mimetype)) {
            throw new Error(`Invalid file type: ${file.filename}`);
          }

          // Save file
          const md5 = await file.md5();
          const key = `documents/${projectId}/${md5}.${file.extname}`;
          const url = await file.saveToDisk("fs", key);

          // Record in DB
          const doc = await wdb.table("documents").insert({
            project_id: projectId,
            filename: file.filename,
            url,
            size: file.size,
            mimetype: file.mimetype,
            created_at: new Date()
          }).returning("*");

          results.push(doc[0]);
        }

        return results;
      }
    }
    ```
  </Tab>

  <Tab title="CSV File Processing" icon="file-csv">
    ```typescript theme={null}
    import Papa from "papaparse";

    class ImportModelClass extends BaseModelClass {
      @upload()
      @transactional()
      async importUsers() {
        const { bufferedFiles } = Sonamu.getContext();
        const file = bufferedFiles?.[0];

        if (!file) {
          throw new Error("No file");
        }

        if (file.mimetype !== "text/csv") {
          throw new Error("CSV file required");
        }

        // Parse CSV (use Buffer directly)
        const text = file.buffer.toString("utf-8");

        const parsed = Papa.parse<UserImportRow>(text, {
          header: true,
          skipEmptyLines: true
        });

        if (parsed.errors.length > 0) {
          throw new Error("CSV parsing failed");
        }

        // Validate and save data
        const wdb = this.getDB("w");
        const results = [];

        for (const row of parsed.data) {
          // Validate
          if (!row.email || !row.name) {
            throw new Error(`Invalid row: ${JSON.stringify(row)}`);
          }

          // Save
          const user = await wdb.table("users")
            .insert({
              email: row.email,
              name: row.name,
              phone: row.phone || null,
              created_at: new Date()
            })
            .returning("*");

          results.push(user[0]);
        }

        return {
          imported: results.length,
          users: results
        };
      }
    }
    ```
  </Tab>

  <Tab title="MD5 Hash-based Storage" icon="hashtag">
    ```typescript theme={null}
    class FileModelClass extends BaseModelClass {
      @upload({ limits: { files: 10 } })
      async uploadWithDeduplication() {
        const { bufferedFiles } = Sonamu.getContext();

        if (!bufferedFiles || bufferedFiles.length === 0) {
          throw new Error("No files");
        }

        const results = [];
        for (const file of bufferedFiles) {
          // Calculate MD5 hash
          const hash = await file.md5();

          // Save file with hash (prevent duplicates)
          const ext = file.extname || "bin";
          const key = `uploads/${hash}.${ext}`;
          const url = await file.saveToDisk("fs", key);

          results.push({
            filename: file.filename,
            hash,
            url,
            size: file.size
          });
        }

        return results;
      }
    }
    ```
  </Tab>

  <Tab title="Stream Mode Large Files" icon="cloud-arrow-up">
    ```typescript theme={null}
    class FileModelClass extends BaseModelClass {
      @upload({
        consume: "stream",
        destination: "s3",
        keyGenerator: (file) => `large-files/${Date.now()}-${file.filename}`,
        limits: { files: 3, fileSize: 100 * 1024 * 1024 }  // 100MB
      })
      async uploadLargeFiles() {
        const { uploadedFiles } = Sonamu.getContext();

        if (!uploadedFiles || uploadedFiles.length === 0) {
          throw new Error("No files");
        }

        // Files are already uploaded to S3
        return {
          files: uploadedFiles.map((file) => ({
            filename: file.filename,
            url: file.url,
            signedUrl: file.signedUrl,
            key: file.key,
            size: file.size,
          })),
        };
      }
    }
    ```
  </Tab>
</Tabs>

## Notes

### Relationship with @api Decorator

The `@upload` decorator automatically creates an API endpoint internally. Therefore, you don't need to use the `@api` decorator separately.

**Automatically configured values:**

* `httpMethod`: `"POST"` (fixed)
* `clients`: `["axios-multipart", "tanstack-mutation-multipart"]` (multipart-specific clients)
* `guards`: `guards` value from `@upload` options is passed to the API
* `description`: `description` value from `@upload` options is passed to the API

```typescript theme={null}
// Use @upload only (recommended)
@upload()
async uploadFile() {
  // ...
}

// Unnecessary - no need to use with @api
@api({ httpMethod: "POST" })  // Unnecessary
@upload()
async uploadFile() {
  // ...
}
```

<Info>
  `@upload` automatically applies optimized settings for file uploads, so there's no need to add the
  `@api` decorator.
</Info>

## Next Steps

<CardGroup cols={2}>
  <Card title="@api" icon="plug" href="/en/api-reference/decorators/api">
    Create API endpoints
  </Card>

  <Card title="@transactional" icon="arrows-rotate" href="/en/api-reference/decorators/transactional">
    Save safely with transactions
  </Card>

  <Card title="Storage Drivers" icon="hard-drive" href="/en/core-concepts/storage/drivers">
    Use S3, Local and other storage
  </Card>

  <Card title="File Handling" icon="file-image" href="/en/guides/file-handling">
    Image/document processing guide
  </Card>
</CardGroup>
