It has been so easy to upload files to S3 with the AWS SDK that Amazon has provided. Unfortunately, not everyone can use AWS services and therefore some prefer to go for alternate solutions like Digital Ocean. They provide a fair amount of good services which are really easy to use and come in pretty handy.

I admit that it's also been pretty easy and straightforward to upload files to the famous Digital Ocean file storage known as Spaces in a NodeJs server that there is just a whole lot of articles out there that talk about it. I haven't seen many (avoiding to say 'any' cause you never know 😁😁) that cover doing that in a NestJs server. Therefore, in this article, I will try to show how easy it is to upload files to Digital Ocean Spaces using the AWS SDK.

Note that in this article I assume the following:

  1. You are already familiar with NestJs
  2. You are already familiar with Digital Ocean Spaces
  3. You have set up a Digital Ocean Spaces "Instance" and already have all necessary API Keys
  4. You already have a NestJs codebase already set up and running.

So just as an exercise, let's try to create a simple Controller that will be expecting a file from a form-data request body and that will be uploading it to Digital Ocean.

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

NestJs is built around a very strong design pattern known as Dependency Injection. Thanks to typescript, it's really easy to manage dependencies because they are just 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 find out how to create an instance of it on its own. Thus, we need to create a custom provider for it and then the actual service 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'}`,
              ),
            );
          }
        },
      );
    });
  }
}

B. Now 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 {}


All that remains now is just adding the module to the main app module and then sending a POST request to /do/spaces with the file attached to it as file and expect to get the URL back and check the file on digital ocean spaces

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/