Uploading files to Digital Ocean Spaces, NestJs
If you've ever used the AWS SDK to upload files to S3, you know how convenient it can be. However, not everyone has the option to leverage AWS services, and that's where DigitalOcean's Spaces comes in. Spaces is an excellent alternative that offers similar functionality and is just as easy to use.
While uploading files to Spaces using a Node.js server is well-documented, resources covering the process with a NestJS server are harder to come by. In this article, I'll walk you through how to seamlessly upload files to DigitalOcean Spaces using the AWS SDK within a NestJS application.
Before we dive in, let's clarify a few assumptions: I'm assuming you're already familiar with NestJS, have experience with DigitalOcean Spaces, and have set up a Spaces instance with the necessary API keys. Additionally, you should have a working NestJS project ready to go.
With those basics covered, let's move on to creating a simple controller that accepts a file from a form-data request body and uploads it to DigitalOcean Spaces. This should be a straightforward exercise and a great way to get comfortable with the process. Let's get started!
1. Creating a Service to Handle File Uploads
One of NestJS's standout features is its strong emphasis on the Dependency Injection (DI) design pattern. DI simplifies managing dependencies in a TypeScript codebase by resolving them automatically based on their types.
For our file upload functionality, we'll create a custom service. Unlike built-in services that NestJS can automatically resolve, custom services require a bit more setup. We'll need to create a custom provider to ensure our service is correctly instantiated and injected where needed.
Before we dive into the code, let's start by installing the AWS SDK. You can do so with one of the following commands:
in case you are using npm:
// npm
npm install aws-sdk
// 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
- NestJs Custom Providers: https://docs.nestjs.com/fundamentals/custom-providers#custom-providers
- AWS NPM SDK: https://www.npmjs.com/package/aws-sdk
- Digital Ocean Spaces: https://www.digitalocean.com/products/spaces/