Skip to content

Mean 16 File Upload

DodaTech 7 min read

title: "File Upload — Handling File Uploads in the MEAN Stack" description: "Implement file upload in the MEAN Stack using Multer with Express, store files locally or in cloud storage, and build Angular upload components." weight: 26 date: 2026-06-28 lastmod: 2026-06-28 tags: [frameworks, mean]

File upload is a common requirement in web applications. In the MEAN stack, Multer handles file uploads on the Express side, and Angular provides the upload UI.

What You'll Learn

You will implement file upload with Multer, serve uploaded files, integrate with cloud storage, and build Angular components with progress tracking.

Why It Matters

File upload is required for user avatars, product images, document management, and many other features. Proper implementation ensures security and good user experience.

Real-World Use

DodaZIP's file sharing portal uses Multer with S3 storage for file uploads, supporting drag-and-drop, chunked uploads for large files, and progress bars.

flowchart LR
    A[Angular Upload] --> B[Multipart Request]
    B --> C[Multer Middleware]
    C --> D[File Validation]
    D --> E[Save File]
    E --> F[Local Storage]
    E --> G[Cloud Storage S3]
    E --> H[Return File URL]
    H --> A
    style C fill:#4a90d9,color:#fff

Multer Setup

Install and configure Multer for file uploads.

npm install multer
// backend/config/upload.js
const multer = require('multer');
const path = require('path');

// Storage configuration
const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    cb(null, 'uploads/');
  },
  filename: (req, file, cb) => {
    const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
    cb(null, uniqueSuffix + path.extname(file.originalname));
  }
});

// File filter
const fileFilter = (req, file, cb) => {
  const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'application/pdf'];
  if (allowedTypes.includes(file.mimetype)) {
    cb(null, true);
  } else {
    cb(new Error('Invalid file type. Only JPEG, PNG, GIF, and PDF are allowed.'), false);
  }
};

const upload = multer({
  storage,
  fileFilter,
  limits: {
    fileSize: 5 * 1024 * 1024 // 5MB
  }
});

module.exports = upload;

Expected output: Multer configured with disk storage, file type filtering, and size limits. Files are saved to the uploads/ directory with unique names.

Upload Route

Create an Express route for file upload.

// backend/routes/uploadRoutes.js
const express = require('express');
const router = express.Router();
const upload = require('../config/upload');
const { authMiddleware } = require('../middleware/auth');

// Single file upload
router.post('/single', authMiddleware, (req, res) => {
  upload.single('file')(req, res, (err) => {
    if (err) {
      if (err.code === 'LIMIT_FILE_SIZE') {
        return res.status(400).json({ error: 'File too large. Maximum size is 5MB.' });
      }
      return res.status(400).json({ error: err.message });
    }

    if (!req.file) {
      return res.status(400).json({ error: 'No file provided' });
    }

    res.status(201).json({
      filename: req.file.filename,
      originalName: req.file.originalname,
      size: req.file.size,
      mimetype: req.file.mimetype,
      url: `/uploads/${req.file.filename}`
    });
  });
});

// Multiple file upload
router.post('/multiple', authMiddleware, (req, res) => {
  upload.array('files', 5)(req, res, (err) => {
    if (err) {
      return res.status(400).json({ error: err.message });
    }

    const files = req.files.map(file => ({
      filename: file.filename,
      originalName: file.originalname,
      size: file.size,
      url: `/uploads/${file.filename}`
    }));

    res.status(201).json({ files });
  });
});

Expected output: POST /api/upload/single accepts a single file. POST /api/upload/multiple accepts up to 5 files. Both return file metadata and URLs.

Serving Static Files

Make the uploads directory accessible from the browser.

// backend/server.js
const path = require('path');
app.use('/uploads', express.static(path.join(__dirname, 'uploads')));

Expected output: Files in the uploads/ directory are accessible at /uploads/filename.jpg. Angular can use the URL directly in img src attributes.

Angular Upload Component

Build an Angular component with file selection and progress tracking.

// src/app/components/file-upload/file-upload.component.ts
import { Component, EventEmitter, Output } from '@angular/core';
import { CommonModule } from '@angular/common';
import { HttpClient, HttpEventType, HttpRequest } from '@angular/common/http';
import { environment } from '../../../environments/environment';

@Component({
  selector: 'app-file-upload',
  standalone: true,
  imports: [CommonModule],
  template: `
    <div class="upload-container"
         (dragover)="onDragOver($event)"
         (drop)="onDrop($event)">
      <input
        type="file"
        #fileInput
        (change)="onFileSelected($event)"
        accept="image/jpeg,image/png,image/gif,application/pdf"
        [attr.multiple]="multiple ? '' : null"
        style="display: none"
      />

      <div *ngIf="!uploading" class="drop-zone" (click)="fileInput.click()">
        <p>Drag and drop files here or click to browse</p>
        <p style="font-size: 12px; color: #666">
          Supported: JPEG, PNG, GIF, PDF (max 5MB each)
        </p>
      </div>

      <div *ngIf="uploading" class="progress-container">
        <p>Uploading... {{ progress }}%</p>
        <div class="progress-bar">
          <div class="progress-fill" [style.width.%]="progress"></div>
        </div>
      </div>

      <div *ngIf="error" class="error">{{ error }}</div>

      <div *ngIf="uploadedFiles.length" class="file-list">
        <div *ngFor="let file of uploadedFiles" class="file-item">
          <img *ngIf="isImage(file.mimetype)"
               [src]="environment.apiUrl + '/../' + file.url"
               width="80" height="80" />
          <span>{{ file.originalName }}</span>
          <span>{{ (file.size / 1024).toFixed(1) }} KB</span>
        </div>
      </div>
    </div>
  `,
  styles: [`
    .drop-zone { border: 2px dashed #ccc; padding: 40px; text-align: center; cursor: pointer; }
    .drop-zone:hover { border-color: #4a90d9; }
    .progress-bar { background: #eee; height: 8px; border-radius: 4px; }
    .progress-fill { background: #4a90d9; height: 100%; border-radius: 4px; transition: width 0.3s; }
    .error { color: red; }
    .file-list { margin-top: 16px; }
    .file-item { display: flex; align-items: center; gap: 12px; padding: 8px; }
  `]
})
export class FileUploadComponent {
  @Output() uploadComplete = new EventEmitter<any[]>();

  uploading = false;
  progress = 0;
  error: string | null = null;
  uploadedFiles: any[] = [];
  multiple = true;

  constructor(private http: HttpClient) {}

  onFileSelected(event: any) {
    const files = event.target.files;
    if (files.length) this.uploadFiles(files);
  }

  onDragOver(event: DragEvent) {
    event.preventDefault();
    event.stopPropagation();
  }

  onDrop(event: DragEvent) {
    event.preventDefault();
    const files = event.dataTransfer?.files;
    if (files?.length) this.uploadFiles(files);
  }

  uploadFiles(files: FileList) {
    const formData = new FormData();
    const endpoint = files.length > 1 ? `${environment.apiUrl}/upload/multiple` : `${environment.apiUrl}/upload/single`;

    for (let i = 0; i < files.length; i++) {
      formData.append(files.length > 1 ? 'files' : 'file', files[i]);
    }

    this.uploading = true;
    this.error = null;
    this.progress = 0;

    const req = new HttpRequest('POST', endpoint, formData, {
      reportProgress: true
    });

    this.http.request(req).subscribe({
      next: (event) => {
        if (event.type === HttpEventType.UploadProgress) {
          this.progress = Math.round(100 * event.loaded / (event.total || 1));
        } else if (event.type === HttpEventType.Response) {
          const result = event.body as any;
          this.uploadedFiles = result.files || [result];
          this.uploading = false;
          this.uploadComplete.emit(this.uploadedFiles);
        }
      },
      error: (err) => {
        this.uploading = false;
        this.error = err.error?.error || 'Upload failed';
      }
    });
  }

  isImage(mimetype: string): boolean {
    return mimetype.startsWith('image/');
  }
}

Expected output: A drag-and-drop file upload component with progress bar, file type validation, preview for images, and support for single and multiple files.

Common Mistakes

  1. Not validating file types on the server: Client-side validation can be bypassed. Always validate file types and sizes on the server.

  2. Not setting file size limits: Without size limits, users can upload very large files that exhaust server storage and bandwidth.

  3. Serving uploads from the Node.js process: For production, use a CDN or cloud storage (S3, Cloudinary). Node.js is not optimized for serving static files.

  4. Not cleaning up failed uploads: If validation fails after the file is partially saved, remove the incomplete file to prevent orphaned files.

  5. Not checking for existing files with the same name: Use unique filenames to prevent overwriting existing uploads.

Practice Questions

  1. What is Multer and what does it do?

Multer is a Node.js middleware for handling multipart/form-data, primarily used for file uploads.

  1. How do you limit file size in Multer?

Set limits.fileSize in the Multer configuration. The value is in bytes.

  1. How do you track upload progress in Angular?

Use HttpRequest with reportProgress: true. Listen for HttpEventType.UploadProgress events.

  1. How do you serve uploaded files from Express?

Use express.static() middleware pointing to the uploads directory.

  1. How do you validate file types in Multer?

Implement a fileFilter function that checks file.mimetype and calls cb(null, true) or cb(new Error()).

Challenge

Build a complete file upload system with: image upload (JPEG, PNG, GIF only, max 2MB), document upload (PDF only, max 10MB), thumbnail generation for images using Sharp, and a file gallery component in Angular.

Frequently Asked Questions

Should I store files locally or in the cloud?

For production, use cloud storage (AWS S3, Cloudinary, Google Cloud Storage). Local storage does not scale across multiple server instances.

How do I handle chunked uploads for large files?

Split files into chunks on the client. Upload each chunk separately. Reassemble on the server. Libraries like resumable.js handle this.

{{< faq "How do I secure uploaded files?" >> Store files outside the public directory. Serve them through an authenticated Express route instead of static middleware. {{< /faq >}}

Can I upload base64-encoded images?

Yes, but multipart upload is more efficient for binary files. Base64 encoding increases size by 33 percent.

How do I handle file deletion?

Create a delete endpoint that removes the file from storage and updates the database record.

Mini Project

Build a user avatar upload system with: Multer configuration for images only, avatar resize with Sharp, Angular upload component with crop preview, and avatar display on the user profile page.

What's Next

Implement {{< ilink "error handling middleware" "Error Handling Middleware" > }} for consistent API error responses.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro