Upload Files to S3 Using Angular and NestJS

Upload Files to S3 Using Angular and NestJS

Sunny Sun lol

An End to End Example using Angular and NestJS

Recently, I worked on an S3 file-uploading feature for an Angular/NestJS App. To my surprise, I have difficulty finding a tutorial with complete and working examples. Thus, I thought it might be useful to walk through the process of file uploading to the S3 (Amazon Simple Storage Service) bucket with Angular and NestJS.

I assume that you have basic knowledge of Angular and NestJS. So I will skip those initial App Setups and focus on the file uploading part.

Prerequisites

You will need these technologies to follow along:

  • Angular 14

  • NestJS version 9

  • NodeJS v14 or above

  • An AWS account

S3 bucket and Access Key/Secret Access Key

To upload files to S3, we need to set up the S3 bucket and IAM user required for NestJS API to access it.

If you don’t have an S3 bucket yet, follow this instruction to create one.

The next step is to log in to your AWS account and navigate to IAM to add a user. Enter a user name and tick the “Access key -Programmatic access” option.

Create IAM UserCreate IAM User

Then, click on the “Next: Permission” button to attach the “AmazonS3FullAccess” policy to the IAM user.

Attach existing policyAttach existing policy

Click on the “Next” button to accept the default setting and create the user. The Access key ID and Secret access key will be shown on the success screen. We can download the CSV file to keep the key values for later use.

Here we create an IAM user with full access to all the S3 buckets under your AWS account. In the production environment, we should create a custom policy to restrict the IAM user to access the particular S3 bucket for file uploading only. But for sake of simplicity, we skip this step in this article.

Now, we have the S3 bucket ready. Let’s build the file upload API with NestJS.

NestJS file uploading

We can create a new NestJS App using Nest CLI.

nest new ng-nest-upload

We need to install the aws-sdk package.

npm install aws-sdk @types/aws-sdk

We also need to install the type definition of multer

npm i -D @types/multer

NestJs makes use of Multer to handle the file upload. Multer is a node.js middleware for handling multipart/form-data, which is primarily used for uploading files. Multer extracts the values of the form text fields into a body object. It also creates a new object for file or multiple files in the request.file or request.files object. The body object contains the values of the text fields of the form, and the file or files object contains the files uploaded via the form.

Let’s create the file upload endpoint which captures and saves the file from FileInterceptor.


  @Post('upload')
  @UseInterceptors(FileInterceptor('file'))
  async uploadFile(
    @UploadedFile() file: Express.Multer.File,
    @Request() req,
  ): Promise {
    const result = await this.fileservice.uploadPublicFile(
      file.buffer,
      file.originalname,
    );
    return new ResponseModel(result); 
  }

We use two decorators here:

  • FileInterceptor: It takes the field name as the first parameter to extract the uploaded file. The field name is sent from the client multipart/form-data, thus you will need to update it to match the name of the field in your code. The FileInterceptor uses Multer under the hood.

  • UploadedFile: It is used to reference the file payload from request.

Save the file to S3 Bucket

In the above controller, we also use FileService to save the file to S3. In FileService, we use aws-sdk v3 to upload a stream object to an S3 bucket. As shown below, we need to specify 3 parameters

  • Bucket: name of the S3 bucket

  • Body: a buffer object representing the file

  • Key: Unique key for the file


@Injectable()
export class FileService {
  async uploadPublicFile(dataBuffer: Buffer, filename: string) {
    try {
      const s3 = new S3();
      const uploadResult = await s3
        .upload({
          Bucket: 'file-uploads',
          Body: dataBuffer,
          Key: `${uuid()}-${filename}`
        })
        .promise();

      return  {
        key: uploadResult.Key,
        url: uploadResult.Location,
      };
    } catch (err) {
      console.log(err);
      return { key: 'error', url: err.message };
    }
  }
}

How is the access configuration to S3 set up in NestJS API? we initialize these configurations at thebootstrap method of main.ts.


async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.setGlobalPrefix('api');
   config.update({
        accessKeyId: '[your S3 Access key]',
        secretAccessKey: '[your S3 secret]',
        region: '[your S3 region]',
      });
  await app.listen(3000);
}
bootstrap();

Again for Sake of simplicity, the Access Key and Secret are hardcoded in the above example. In the real-life App, they should come from environment variables.

Angular client

To upload files from an Angular App, we use the *File *interface that provides information about files and allows JavaScript on a web page to access their content.

The following represents a file select field and a “Choose file” button. This plain button allows users to access local files from a browser. We can use the accept attribute to limit the type of files to upload.

<input type="file" [accept]="acceptedFileExtensions" (change)="attachFile($event)">

Unfortunately, we can’t change the style of the default file select button. The solution is to use another custom button to invoke the default button. In the code below, we hide the default button and style the custom button with CSS class fileButton.


  
  
allowed file types(JPG, PNG, GIF)

In the UploadComponent, we use @ViewChild decorator to get a reference to the hidden file select button in the template.

@ViewChild(‘fileInput’, { static: false })
 fileInput: ElementRef | undefined;

Instead of calling the click handler within the Html template, we can invoke the hidden button click within the component class as below.

this.fileInput.nativeElement.click();

To send the file to the NestJS API, we construct the formData and post a request to the NestJS endpoint as below.


  attachFile(event: any) {
    const file: File = event.target.files?.[0];
    if (file && this.isValid(file)) {
      this.errorMessage = '';
      const formData = new FormData();
      formData.append('file', file, file.name);
      const uploadFileHeaders = new HttpHeaders({
        Accept: `application/json, text/plain, */*`,
      });
      this.httpClient
        .post('/api/file-upload/upload', formData, {
          headers: uploadFileHeaders,
        })
        .subscribe({
          next: (response) => {
            this.successMessage = `Document ${file.name} is uploaded successfully`;
          },
          error: (error) => {
            this.errorMessage = `failed to upload document.`;
            return error;
          },
        });
    }
  }

Please note that the post request header should not include the content type.

Further consideration

In this contrived example, authentication isn’t being implemented. In real-life applications, you may consider using a JWT token to enforce security between the Angular client and the backend.

Another thing to consider is the safety of the uploaded files. Although we have implemented the validation of file extensions in the Angular client, it isn’t enough. It is still possible for a file with malicious content to slip in. A better approach is to set up a staging S3 bucket with an anti-virus scanning service. Any uploaded file will need to be scanned before it is moved to the final bucket for consumption. But this topic is out of the scope of this article and here is a good starting point.

By performing these checks on the uploaded file, you can ensure that it is not malicious and protect your system from potential harm.

Summary

In this article, we walk through an end-to-end example of uploading a file to S3 Bucket from an Angular client via NestJS API. With the built-in decorators, NestJS makes it really easy to handle the file-uploading process.

The complete source code can be found in this GitHub repo .

Happy programming!