# 4. Integrate file upload

Our API is starting to look great now that we can add new stories. But it would be even better if we could attach some cute pictures to our stories, right?

happy cay saying yes

# Configure Azure Storage module

We will use Azure Blob Storage to store pets images in the cloud. It can be used to store any kind of file, and is also capable of hosting static websites.

As you already created a storage account in Step 2, you only need to integrate the @nestjs/azure-storage package with this command:

npm install @nestjs/azure-storage

Open the file src/app.module.ts and add the AzureStorageModule to the module imports:

@Module({
  imports: [
    AzureStorageModule.withConfig({
      sasKey: process.env.AZURE_STORAGE_SAS_KEY,
      accountName: process.env.AZURE_STORAGE_ACCOUNT,
      containerName: 'funpets-images',
    }),
    ...
  ]

Don't forget to add the missing imports at the top:

import { AzureStorageModule } from '@nestjs/azure-storage';

# Handle file upload

Now let's update the POST /api/stories endpoint to add support for image upload.

Open src/stories/stories.controller.ts and update the function you created to create new stories:

  • Add @UseInterceptors(AzureStorageFileInterceptor('file')) just below the @Post() annotation.
  • Add @UploadedFile() file: UploadedFileMetadata in the function parameters.

Don't forget to also add the missing imports at the top:

import { FileInterceptor } from '@nestjs/platform-express';
import { AzureStorageFileInterceptor, UploadedFileMetadata } from '@nestjs/azure-storage';
import { UseInterceptors, UploadedFile } from '@nestjs/common';

The AzureStorageFileInterceptor will directly upload the file to Azure Storage container funpets-images specified in the module configuration, and will fill in the stored file url in file.storageUrl.

Once you have the storage URL you can set the imageUrl of the created Story entity.

Your final function should looks like this:

@Post()
@UseInterceptors(AzureStorageFileInterceptor('file'))
async createStory(
  @Body()
  data: Partial<Story>,
  @UploadedFile()
  file: UploadedFileMetadata,
): Promise<Story> {
  const story = new Story(data);
  if (!story.createdAt) {
    story.createdAt = new Date();
  }
  if (file) {
    story.imageUrl = file.storageUrl || null;
  }
  return await this.storiesRepository.create(story);
}

# Test your endpoint

After you finished the modifications, start your server using the functions emulator:

npm run start:azure

After the server is started, you can test if uploading file works using curl:

curl http://localhost:7071/api/stories \
  -F "file=@<path_to_image_file>" \
  -F "animal=cat" \
  -F "description=Happy cat"

You can download and use the the happy cat image to test the file upload if don't have an image at hand.

Note

Using the -F curl option will automatically set the request content type to multipart/form-data which is required for Nest.js file upload support. Note that in that case, the payload for the Story property will also have to be form data and not JSON, as you can see in the curl command.

# Limit accepted file type/size

Your API now support file uploads, but surely you don't want any file to be uploaded and may want to set some reasonable limits on file size?

Just like the base NestJS FileInterceptor, the AzureStorageFileInterceptor() decorator supports a second options argument. The options object is of type MulterOptions and can be used to achieve what we want, using the limits and fileFilter properties. This is the same object used by the multer constructor.

Note

Multer is the underlying Express middleware used by NestJS to handle file uploads.

Now it's your time to work and find out how to restrict file uploads to support only:

  • A maximum file size of 2MB
  • png and jpeg image types

Some hints to get started:

  • Look at the limits and fileFilter options to see how they work.
  • You can get the uploaded file name using file.originalname.
  • Explore Node.js path module to get extract extension from a file name.

Don't forget to test your solution with various scenario using curl, to make sure your API accepts/rejects files properly!

Tip

you can use mkfile <size[k|m]> <filename> to generate dummy files with a given size (for Windows users: fsutil file createnew <filename> <size_in_bytes>).

# Redeploy

Once everything works locally let's deploy your latest changes:

# Build your app
npm run build

# Create an archive from your local files and publish it
# Don't forget to change the name with the one you used previously
func azure functionapp publish funpets-api --nozip

Then run again the previous curl command against your deployed API URL to check that everything works fine:

curl https://<your-funpets-api>.azurewebsites.net/api/stories
  -F "file=@<path_to_image_file>" \
  -F "animal=cat" \
  -F "description=Happy cat"