Have you ever used the AWS SDK to easily upload files to S3? It's super convenient, but not everyone has the option to use AWS services. That's where Digital Ocean's Spaces file storage comes in - it's a great alternative with a range of useful services that are easy to use.

While it's straightforward to upload files to Spaces using a Node.js server, I haven't come across many resources that cover the process of uploading files to Spaces using a NestJS server (avoiding to say 'any' cause you never know 😁😁).

In this article, I'll show you how easy it is to upload files to Digital Ocean Spaces using the AWS SDK in a NestJS server.

Before we get started, I want to make a few assumptions clear:

I'm assuming that you are already familiar with NestJS, Digital Ocean Spaces, and have set up a Spaces instance and obtained the necessary API keys. Additionally, I'm assuming that you already have a NestJS codebase up and running.

With that out of the way, let's try to create a simple controller that will accept a file from a form-data request body and upload it to Digital Ocean Spaces. It should be a fairly straightforward exercise, and a good way to get familiar with the process.

Let's get started!

1. Let's create a service that will contain all the logic to upload the file

One of the key features of NestJS is its strong reliance on the Dependency Injection design pattern. This makes it easy to manage dependencies in a TypeScript codebase, as they are simply resolved by type.

The service that we are about to create is not a built-in service that we can just dump anywhere we want in the codebase and expect the Dependency Injection to figure out how to create an instance of it on its own. We need to create a custom provider for it and then the actual implementation will come later.

Let's start by installing the AWS SDK using one of the following commands

in case you are using npm:

npm install aws-sdk

or Yarn:

yarn add  aws-sdk

In our codebase, under src, let's create a directory called SpacesModule that contains  a directory called SpacesService that contains 2 files, index.ts and doSpacesService.ts. index.ts will contain the provider and doSpacesService.ts will be the actual service.

// index.ts
import * as AWS from 'aws-sdk';
import { Provider } from '@nestjs/common';

// Unique identifier of the service in the dependency injection layer
export const DoSpacesServiceLib = 'lib:do-spaces-service';

// Creation of the value that the provider will always be returning.
// An actual AWS.S3 instance
const spacesEndpoint = new AWS.Endpoint('fra1.digitaloceanspaces.com');

const S3 = new AWS.S3({
  endpoint: spacesEndpoint.href,
  credentials: new AWS.Credentials({
    accessKeyId: '<put-your-digital-ocean-spaces-key-here>',
    secretAccessKey: '<put-your-digital-ocean-spaces-secret-here>',
  }),
});

// Now comes the provider
export const DoSpacesServicerovider: Provider<AWS.S3> = {
  provide: DoSpacesServiceLib,
  useValue: S3,
};

// This is just a simple interface that represents an uploaded file object 
export interface UploadedMulterFileI {
  fieldname: string;
  originalname: string;
  encoding: string;
  mimetype: string;
  buffer: Buffer;
  size: number;
}

Now, let's create the service with a method called uploadFile

// doSpacesService.ts
import { Inject, Injectable } from '@nestjs/common';
import * as AWS from 'aws-sdk';
import {
  DoSpacesServiceLib,
  DoSpacesServicerovider,
} from './doSpacesService';

// Typical nestJs service
@Injectable()
export class DoSpacesService {
  constructor(@Inject(DoSpacesServiceLib) private readonly s3: AWS.S3) {}

  async uploadFile(file: UploadedMulterFileI) {
    // Precaution to avoid having 2 files with the same name
    const fileName = `${Date.now()}-${
      file.originalname
    }`;

    // Return a promise that resolves only when the file upload is complete
    return new Promise((resolve, reject) => {
      this.s3.putObject(
        {
          Bucket: '<put-here-the-name-of-your-spaces-bucket>',
          Key: fileName,
          Body: file.buffer,
          ACL: 'public-read',
        },
        (error: AWS.AWSError) => {
          if (!error) {
            resolve(`<put-here-the-public-link-to-your-spaces-instance>/${fileName}`);
          } else {
            reject(
              new Error(
                `DoSpacesService_ERROR: ${error.message || 'Something went wrong'}`,
              ),
            );
          }
        },
      );
    });
  }
}

2. As the last pieces of the puzzle, let's create a module to wrap everything and the Controller

Under the SpacesModule directory, let's create 2 files, spaces.module.ts and spaces.controller.ts at this point, our SpacesModule directory looks like this:

src-
    |
    |SpacesModule
                |
                |-SpacesService
                |             |
                |             | doSpacesService.ts
                |             | index.ts
                | spaces.controller.ts
                | spaces.module.ts
                

in spaces.controller.ts let's have the following

import {
  Controller,
  UploadedFile,
  UseInterceptors,
  Post
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { DoSpacesService } from './SpacesService/doSpacesService';
import { DoSpacesServicerovider, UploadedMulterFileI } from './SpacesService';

// just a typical nestJs controller
@Controller('/api/v1/do')
export class CommonController {
  constructor(
    private readonly doSpacesService: DoSpacesService,
  ) {}

  @UseInterceptors(FileInterceptor('file'))
  @Post('spaces')
  async uploadFile(@UploadedFile() file: UploadedMulterFileI) {
    const url = await this.doSpacesService.uploadFile(file);

    return {
      url,
    };
  }
}

in spaces.module.ts let's have the following

import { Module } from '@nestjs/common';
import { SpacesController } from './spaces.controller';
import { DoSpacesService } from './SpacesService/doSpacesService';
import { DoSpacesServicerovider } from './SpacesService';

@Module({
  imports: [],
  controllers: [SpacesController],
  // provide both the service and the custom provider
  providers: [DoSpacesServicerovider, DoSpacesService],
})
export class SpacesModule {}


We've covered all the steps necessary to handle file uploads to Digital Ocean Spaces using a NestJS server and the AWS SDK. All that's left to do now is to add the module we created to the main app module and then send a POST request to the /do/spaces endpoint with the file attached as a form field named file. If everything is set up correctly, you should receive a URL back in the response, and you can check the file on Digital Ocean Spaces to confirm that the upload was successful.

References

  1. NestJs Custom Providers: https://docs.nestjs.com/fundamentals/custom-providers#custom-providers
  2. AWS NPM SDK: https://www.npmjs.com/package/aws-sdk
  3. Digital Ocean Spaces: https://www.digitalocean.com/products/spaces/