This post originally appeared on my blog here.
Introduction
Today I am going to be running through how to use an S3 object storage provider.
(Just want to see the code? The GitHub is here)
For those who don't know S3 object storage is a cloud service for hosting files. It is accessible via an API, which means it can easily be integrated into your projects. There are hundreds of uses cases but some of the most common involve hosting user-generated content and allowing users to upload profile images.
Some of the most popular providers for S3 storage include Amazon AWS, Vultr and Digital Ocean. They all provide the same service but have a few differences when it comes to price, locations, capacities and bandwidths, so it's worth looking around to see which one suits your needs best.
My first experience with S3 was using AWS. AWS is great.....but its also very confusing, especially for a backend developer like me who tries to stay clear of DevOps as much as he can. I trawled through the AWS documentation trying to understand how to implement the S3 service and after many hours of playing with buckets, policies and IAM roles I got it working. After my ordeal I decided to try other providers to see how the implementations differ (In the hope of finding a simpler solution). It turns out that implementations are the same across providers!
So, I will run you through a very simple example of how to implement a basic S3 object storage in Nodejs. The example I am going to give uses Express and multer for the file upload, however the object storage code is framework agnostic and only requires the aws-sdk.
Preparing our Node.js project
Before we can connect to our S3 provider there are 4 things you will need. These are:
- The buckets endpoint URL
- The bucket name
- Access key
- Secret access key
These should be provided to you once you have set up your bucket through your chosen providers dashboard. You will want to ensure that your keys are kept private and securely. So in this example we will use dotenv environment variables.
Firstly, lets create our
.env file in our project root:
S3_BUCKET_NAME=your_bucket_name // e.g my-bucket S3_ENDPOINT_URL=your_endpoint_url // e.g S3_ACCESS_KEY=your_access_key S3_SECRET_KEY=your_secret_access_key
Now we have the information for creating a connection, lets go ahead and install the packages for initialising a connection.
The first thing we need is the
aws-sdk this is the npm package used for connecting and interacting with an S3 storage. Run the following command to install:
npm install aws-sdk
In this example we are using TypeScript so we can also install some type definitions. If you are using JavaScript then you can ignore this step.
npm install --save-dev @aws-sdk/types
Setting up the connection
Once installed we can create our
connection.ts :
import * as S3 from 'aws-sdk/clients/s3'; export default function Connect(path: string | null = ''): S3 { return new S3({ apiVersion: 'latest', endpoint: `${process.env.S3_ENDPOINT_URL}${path}`, credentials: { accessKeyId: process.env.S3_ACCESS_KEY, secretAccessKey: process.env.S3_SECRET_KEY, }, }); }
Lets go through this code line by line. So firstly we import the S3 client from the aws-sdk. The aws-sdk includes a lot of features, so we only need to import the S3 client for this implementation.
Next we create our Connect function. This function will new up an S3 client using the credentials that we stored in our environment variables.
Our connect function takes in an optional
path parameter. When this is set we can specify the path that we want to upload our file to. For example we may want to upload an image to a subdirectory called images. So we would set the path to 'images'. This path is then appended to the endpoint URL. So as an example our endpoint now becomes. If we don't set the path parameter the connection will default to the buckets root.
In our configuration we also provide an S3 API version. In this example I will use latest but you may want to pick a version that works for you. You can read up more about API versions and why you should pick one here.
Uploading a file
Now we have a working S3 client instance we can use it to upload files. Lets create a function for uploading a file. For this example we are using multer, so TypeScript users you can install the types with
npm i --save-dev @types/multer.
Our
upload.ts will look like this:
import { PutObjectOutput, PutObjectRequest } from 'aws-sdk/clients/s3'; import {AWSError} from 'aws-sdk/lib/error'; import * as S3 from 'aws-sdk/clients/s3'; import Connect from './connection'; export default async function Upload(bucket: string, file: Express.Multer.File, objectName: string, path: string | null = null): Promise<string> { return new Promise<string>((resolve, reject) => { const s3: S3 = Connect(path); const params: PutObjectRequest = { Bucket: bucket, Key: objectName, Body: file.buffer, ACL: 'public-read', ContentType: file.mimetype }; s3.putObject(params, (err: AWSError, data: PutObjectOutput) => { if (err) reject(err); resolve(`${process.env.S3_ENDPOINT_URL}${bucket}/${path}/${objectName}`); }); }); }
In our Upload function we are passing in 4 parameters:
Our Upload function will return a Promise. This will resolve the URL of our uploaded file once the S3 client has finished uploading.
Inside our new Promise, we first use our Connect function to get an initialised S3 client, passing through our optional
path parameter.
Then we create our S3 request parameters. In the parameters we set 5 options:
Next we call the putObject method on the S3 client. We pass in our request parameters above, and define a callback. The callback will give us an error if the upload fails. So we can check if this has a value in our callback and reject our Promise if there is an error. If there is no error then we can resolve our promise with the URL of our object. We construct the URL of our uploaded object using the endpoint URL, bucket name, path and object name. So as an example if uploaded image.gif to an images folder inside our my-bucket, then the URL would be
Deleting a file
When it comes to deleting a file the process is very similar to upload.
We can create a
delete.ts:
import {DeleteObjectOutput, DeleteObjectRequest} from 'aws-sdk/clients/s3'; import {AWSError} from 'aws-sdk/lib/error'; import * as S3 from 'aws-sdk/clients/s3'; import Connect from './connection'; export default async function Delete(bucket: string, objectName: string, path: string | null = null): Promise<DeleteObjectOutput> { return new Promise<DeleteObjectOutput>((resolve, reject) => { const s3: S3 = Connect(path); const params: DeleteObjectRequest = { Bucket: bucket, Key: objectName }; s3.deleteObject(params, (err: AWSError, data: DeleteObjectOutput) => { if (err) reject(err); resolve(data); }); }); }
This function takes in 3 of the parameters we have seen before:
Inside our Promise we use our Connect function to get an initialised S3 client.
We create our request parameters. Setting the
Bucket and
Key options using our functions parameters.
Then we use the
deleteObject method on the client, passing in our request parameters and defining a callback. Just like before we check if the callback has errored and reject the promise if an error occurs.
If no error occurs then we resolve the
deleteObject response.
Setting up our Express endpoints
We have defined some functions to connect to our S3 provider, upload objects and delete objects. The next question is how do we use them?
We will use Express and Multer as an example to demonstrate how to use them.
Using our Express app we can define a POST endpoint like the following:
app.post( '/upload', multer().single('formFile'), async (req, res) => { if(!req.file) res.status(400).send('Bad Request: No file was uploaded'); // If you want to retain the original filename and extension just use originalname like below // const filename: string = req.file.originalname; const fileExtension: string = req.file.originalname.split('.').pop(); const filename: string = `my-custom-filename.${fileExtension}`; const url: string = await Upload(process.env.S3_BUCKET_NAME, req.file, filename, 'images/logo'); res.status(201).send(url); });
This creates an endpoint called /upload which accepts multi-part form data. We use the multer middleware with this endpoint. The multer middleware will look in the submitted form data for the field with the key
formFile. This key should be paired with a file. The middleware then attaches the file object to the request under the property
file .
In our handler we check that a file has been supplied and throw a Bad Request response if none was sent.
In the example I have shown how to use a custom filename. We read the file extension from our files original name first. Then we create a new filename, appending the original file extension e.g
my-custom-filename.gif .
Next we call our Upload function. We pass in the bucket name stored in our environment variables; the file in the request; our custom filename; and in the example I am uploading to the subdirectory
images/logo.
After awaiting our Upload we will have the URL of the uploaded file and we can send this in our endpoints response object.
If you would like to see how to use the delete function with an Express endpoint then take a look at the example project.
Example project
I have created a full working example project on GitHub which uses the code we have gone through today. Check it out here.
Discussion (0)