# How TREK Manages File Uploads: NestJS and Multer Architecture Explained

> Discover how TREK manages file uploads with NestJS and Multer. Learn about its efficient architecture, security layers, and storage strategies.

- Repository: [Maurice/TREK](https://github.com/mauriceboe/TREK)
- Tags: architecture
- Published: 2026-07-03

---

**TREK handles file uploads through a centralized NestJS pipeline using Multer middleware, with dedicated storage directories, strict size limits, and multi-layered security checks including demo-mode safeguards and permission validation.**

The open-source TREK travel management application processes all user-generated content—trip covers, avatars, generic attachments, and backup archives—through a consistent upload architecture. This implementation leverages standard NestJS patterns while enforcing security controls through environment variables and service-level permission checks. Understanding how TREK manages file uploads reveals enterprise-grade patterns for handling multipart form data in Node.js applications.

## NestJS and Multer Upload Pipeline

TREK implements file handling through the standard **NestJS FileInterceptor** pattern combined with **Multer** disk storage. Every upload endpoint decorates controller methods with `@UseInterceptors(FileInterceptor(...))`, which configures Multer to parse multipart/form-data requests before they reach the route handler.

In [`server/src/nest/files/files.controller.ts`](https://github.com/mauriceboe/TREK/blob/main/server/src/nest/files/files.controller.ts), the generic file upload endpoint uses this configuration:

```typescript
@Post()
@UseInterceptors(FileInterceptor('file', UPLOAD))
async upload(
  @CurrentUser() user: User,
  @Param('tripId') tripId: string,
  @UploadedFile() file: Express.Multer.File | undefined,
  @Body() body: { place_id?: string; description?: string; reservation_id?: string },
) {
  // Business logic and persistence
  const created = this.files.createFile(tripId, file!, user.id, {
    place_id: body.place_id,
    description: body.description,
    reservation_id: body.reservation_id,
  });
  this.files.broadcast(tripId, 'file:created', { file: created });
  return { file: created };
}

```

The Multer configuration object (`UPLOAD`, `COVER_UPLOAD`, or `AVATAR_UPLOAD`) defines the storage engine, file filters, and size limits. According to the source code in [`server/src/nest/trips/trips.controller.ts`](https://github.com/mauriceboe/TREK/blob/main/server/src/nest/trips/trips.controller.ts), the cover upload uses `diskStorage` with a custom destination callback that ensures directories exist before writing.

## Storage Configuration and Directory Structure

TREK organizes uploaded files into purpose-specific directories under the `/uploads` root. The storage locations are:

- **Covers**: `uploads/covers` (trip cover images)
- **Avatars**: `uploads/avatars` (user profile pictures)
- **Files**: `uploads/files` (generic trip attachments)
- **Temporary**: System temp directory (backup uploads only)

The destination logic in [`server/src/nest/files/files.controller.ts`](https://github.com/mauriceboe/TREK/blob/main/server/src/nest/files/files.controller.ts) creates directories recursively if they do not exist:

```typescript
destination: (req, file, cb) => {
  const dir = fileService.filesDir;
  if (!fs.existsSync(dir)) {
    fs.mkdirSync(dir, { recursive: true });
  }
  cb(null, dir);
}

```

Backup uploads follow a different pattern. In [`server/src/nest/backup/backup.controller.ts`](https://github.com/mauriceboe/TREK/blob/main/server/src/nest/backup/backup.controller.ts), files are written to a temporary directory returned by `getUploadTmpDir()` and deleted immediately after processing using a `finally` block with `fs.unlinkSync`.

## File Size Limits and Validation

TREK enforces strict **file size limits** through Multer configuration objects, with different thresholds for each upload type:

- **Cover images**: **20 MiB** (`MAX_COVER_SIZE`)
- **Avatars**: **5 MiB** (inline `limits.fileSize`)
- **Generic files**: **50 MiB** (`MAX_FILE_SIZE` from `fileService`)
- **Backup archives**: **~500 MiB** (`MAX_BACKUP_UPLOAD_SIZE`, configurable via `BACKUP_UPLOAD_LIMIT_MB`)

The size validation occurs at the middleware level before the controller method executes. In [`server/src/nest/trips/trips.controller.ts`](https://github.com/mauriceboe/TREK/blob/main/server/src/nest/trips/trips.controller.ts), the cover upload configuration defines:

```typescript
export const COVER_UPLOAD = {
  storage: diskStorage({ /* ... */ }),
  limits: { fileSize: MAX_COVER_SIZE }, // 20 * 1024 * 1024
  fileFilter: (req, file, cb) => { /* MIME type check */ }
};

```

When a file exceeds these limits, Multer throws an error that [`server/src/nest/common/trek-exception.filter.ts`](https://github.com/mauriceboe/TREK/blob/main/server/src/nest/common/trek-exception.filter.ts) transforms into HTTP **413 Payload Too Large** or generic **400 Bad Request** responses.

## Security Safeguards and Permission Controls

TREK implements three layers of security validation before accepting any file: **demo-mode blocks**, **extension filtering**, and **permission checks**.

### Demo Mode Protection

When `DEMO_MODE` is set to `true` in the environment and the authenticated user matches a demo email address, all upload endpoints immediately return **403 Forbidden** with the message "Uploads are disabled in demo mode." This check appears in [`server/src/nest/files/files.controller.ts`](https://github.com/mauriceboe/TREK/blob/main/server/src/nest/files/files.controller.ts) and [`server/src/nest/auth/auth.controller.ts`](https://github.com/mauriceboe/TREK/blob/main/server/src/nest/auth/auth.controller.ts):

```typescript
if (process.env.DEMO_MODE?.toLowerCase() === 'true' && isDemoEmail(user.email)) {
  throw new HttpException({ error: 'Uploads are disabled in demo mode.' }, 403);
}

```

### File Type Validation

Each upload configuration includes a `fileFilter` that validates extensions and MIME types. The system rejects files matching `BLOCKED_EXTENSIONS` and specifically blocks SVG uploads for images to prevent XSS attacks. In [`server/src/nest/auth/auth.controller.ts`](https://github.com/mauriceboe/TREK/blob/main/server/src/nest/auth/auth.controller.ts), the avatar filter restricts uploads to images while excluding SVGs.

### Permission Verification

After passing Multer's validation, controllers verify user permissions through service-layer checks. The files controller calls `this.files.can()` to validate `file_upload`, `file_edit`, or `file_delete` permissions, while the trips controller checks `trip_cover_upload` permissions. Failure results in an immediate **403** response before any file persistence occurs.

## Error Handling and Cleanup

TREK centralizes upload error handling through [`server/src/nest/common/trek-exception.filter.ts`](https://github.com/mauriceboe/TREK/blob/main/server/src/nest/common/trek-exception.filter.ts), which normalizes Multer errors into appropriate HTTP status codes. Custom validation errors (such as blocked extensions) use the i18n key `files.uploadErrorType` for localization.

Cleanup operations vary by upload type:

- **Backup uploads**: Automatically deleted from the temporary directory after processing via `fs.unlinkSync` in a `finally` block
- **Generic files**: Soft-delete and permanent deletion logic delegated to `FilesService`
- **Cover images**: Replaced in storage when new covers are uploaded, with old file cleanup handled by the service layer

## Summary

- TREK uses **NestJS FileInterceptor** with **Multer diskStorage** for all multipart uploads, writing files to segregated directories under `/uploads`.
- **File size limits** are enforced at the middleware level: 20 MiB for covers, 5 MiB for avatars, 50 MiB for generic files, and ~500 MiB for backup archives.
- **Security layers** include demo-mode blocking (via `DEMO_MODE` environment variable), extension/MIME filtering (blocking `BLOCKED_EXTENSIONS` and SVGs), and service-layer permission checks.
- **Error handling** is centralized in [`trek-exception.filter.ts`](https://github.com/mauriceboe/TREK/blob/main/trek-exception.filter.ts), converting Multer errors to HTTP 400/413 responses.
- **Cleanup** is automatic for temporary backup files, while production files use service-layer trash and deletion workflows.

## Frequently Asked Questions

### What is the maximum file size allowed for uploads in TREK?

TREK applies different size limits depending on the upload type. Cover images are limited to **20 MiB** (`MAX_COVER_SIZE`), avatars to **5 MiB**, generic trip files to **50 MiB** (`MAX_FILE_SIZE`), and backup archives to approximately **500 MiB** (configurable via `BACKUP_UPLOAD_LIMIT_MB`). These limits are enforced by Multer's `limits.fileSize` configuration before the file reaches the controller.

### How does TREK prevent malicious file uploads?

TREK implements multiple safeguards: a `fileFilter` function rejects blocked extensions and dangerous MIME types (such as SVGs for images), the `DEMO_MODE` environment variable disables uploads entirely for demo instances, and service-layer permission checks verify the user holds `file_upload` or `trip_cover_upload` capabilities before processing. Additionally, the [`trek-exception.filter.ts`](https://github.com/mauriceboe/TREK/blob/main/trek-exception.filter.ts) normalizes errors to prevent information leakage.

### Where does TREK store uploaded files?

Uploaded files are stored in dedicated subdirectories under `/uploads`: `uploads/covers` for trip cover images, `uploads/avatars` for user profile pictures, and `uploads/files` for generic attachments. Backup uploads are temporarily stored in a system temp directory returned by `getUploadTmpDir()` and deleted immediately after processing. The destination callbacks ensure directories exist by calling `fs.mkdirSync` with `recursive: true`.

### Can I upload files in TREK's demo mode?

No. When `DEMO_MODE` is set to `true` and the authenticated user matches a demo email pattern, all upload endpoints in `FilesController`, `AuthController`, and `TripsController` immediately return HTTP **403 Forbidden** with the message "Uploads are disabled in demo mode." This prevents content proliferation in public demonstration instances.