Storing Images in S3 from Node Server


This video is part of the following playlists:


Learn how to store your web app's files in an s3 bucket. Upload, Download, Update, and Delete images in an s3 bucket, from a node.js application.

Uploading an image goes through the express server allowing us to modify the image before it's stored in the s3 bucket. Downloading the image happens directly from the s3 bucket to put less strain on the server and make it easier to integrate our bucket with a CDN in the future.

Chapters:

  • 0:00​ Intro
  • 3:17 Post a photo with multipart/form-data
  • 5:06 Multer
  • 8:40 Create an S3 Bucket
  • 11:38 IAM User and Policy
  • 16:36 AWS SDK S3 Client
  • 19:00 Uploading an image to S3
  • 22:07 Updating an image
  • 23:18 Random Image Names
  • 25:16 Resizing Images
  • 27:36 Saving data to the database
  • 29:55 Getting images with signed url
  • 35:28 Deleting an image
  • 38:19 Summary

Code

Examples of how to upload, download and delete files from S3 bucket using multer and the AWS SDK for JavaScript v3 Node.js.

https://github.com/meech-ward/s3-get-put-and-delete

Libraries:

Note

I used Prisma with MySQL for the database and tailwind for the styling. These are irrelevant details for this example. Any database or styling would work. The important part is how the Node.js backend communicates with S3.

Posting an image to the server

To post an image to the server, the client sends a multipart/form-data request to the server with the image data and any other data that the client wants to send.

HTML:

<form action="/posts" method="POST" enctype="multipart/form-data">
   <input type="file" name="image" accept="image/*"/>
   <input type="text" name="caption" placeholder="Caption"/>
   <button type="submit">Submit</button>
</form>

React/Next:

export default function NewPost() {  
  const [file, setFile] = useState()
  const [caption, setCaption] = useState("")

  const submit = async event => {
    event.preventDefault()

    const formData = new FormData();
    formData.append("image", file)
    formData.append("caption", caption)
    await axios.post("/api/posts", formData, { headers: {'Content-Type': 'multipart/form-data'}})
  }

  return (
     <form onSubmit={submit}>
       <input onChange={e => setFile(e.target.files[0])} type="file" accept="image/*"></input>
       <input value={caption} onChange={e => setCaption(e.target.value)} type="text" placeholder='Caption'></input>
       <button type="submit">Submit</button>
     </form>
  )
}

Processing the image

The server then accepts the image data using multer and keeps the image in memory so it can be easily modified and sent to S3. The app's in this example modify the image by resizing it using sharp. At all times the image is stored in memory as a buffer.

import multer from 'multer'
import sharp from 'sharp'

const storage = multer.memoryStorage()
const upload = multer({ storage: storage })

app.post('/posts', upload.single('image'), async (req, res) => {
  const file = req.file 
  const caption = req.body.caption

  const fileBuffer = await sharp(file.buffer)
    .resize({ height: 1920, width: 1080, fit: "contain" })
    .toBuffer()

  // ...
})

PUTing the image to S3

Next we need to send the image to S3. First S3 needs to be configured with the correct credentials.

import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3"

import dotenv from 'dotenv'

dotenv.config()

const bucketName = process.env.AWS_BUCKET_NAME
const region = process.env.AWS_BUCKET_REGION
const accessKeyId = process.env.AWS_ACCESS_KEY
const secretAccessKey = process.env.AWS_SECRET_ACCESS_KEY

const s3Client = new S3Client({
  region,
  credentials: {
    accessKeyId,
    secretAccessKey
  }
})

Then the image buffer data can be sent to S3 using the PutObjectCommand. Since the images in S3 all need to have unique names, we can use the crypto library to generate a unique unguessable name.

import crypto from 'crypto'

const generateFileName = (bytes = 32) => crypto.randomBytes(bytes).toString('hex')

app.post('/posts', upload.single('image'), async (req, res) => {
  const file = req.file 
  const caption = req.body.caption

  const fileBuffer = await sharp(file.buffer)
    .resize({ height: 1920, width: 1080, fit: "contain" })
    .toBuffer()

  // Configure the upload details to send to S3
  const fileName = generateFileName()
  const uploadParams = {
    Bucket: bucketName,
    Body: fileBuffer,
    Key: fileName,
    ContentType: file.mimetype
  }

  // Send the upload to S3
  await s3Client.send(new PutObjectCommand(uploadParams));

  // Save the image name to the database. Any other req.body data can be saved here too but we don't need any other image data.
  const post = await prisma.posts.create({
    data: {
      imageName,
      caption,
    }
  })

  res.send(post)
})

Generating signed URL

Once the images are being successfully uploaded to S3, we need to generate a signed URL so the client can GET the image from S3. The database only stores the image name, so we generate a signed URL using the image name.

import { GetObjectCommand } from "@aws-sdk/client-s3"
import { getSignedUrl } from "@aws-sdk/s3-request-presigner"

app.get("/", async (req, res) => {
  const posts = await prisma.posts.findMany({ orderBy: [{ created: 'desc' }] }) // Get all posts from the database

  for (let post of posts) { // For each post, generate a signed URL and save it to the post object
    post.imageUrl = await getSignedUrl(
      s3Client,
      new GetObjectCommand({
        Bucket: bucketName,
        Key: imageName
      }),
      { expiresIn: 60 }// 60 seconds
    )
  }

  res.send(posts)
})

https://aws.amazon.com/blogs/developer/generate-presigned-url-modular-aws-sdk-javascript/

Then any client can GET the image from S3 using the signed URL in the src of an img tag.

DELETEing the image from S3

If you want to delete the image from S3, you can use the DeleteObjectCommand passing in the image name, then delete the corresponding post from the database.

app.delete("/api/posts/:id", async (req, res) => {
  const id = +req.params.id
  const post = await prisma.posts.findUnique({where: {id}}) 

  const deleteParams = {
    Bucket: bucketName,
    Key: post.imageName,
  }

  return s3Client.send(new DeleteObjectCommand(deleteParams))

  await prisma.posts.delete({where: {id}})
  res.send(post)
})

Find an issue with this page? Fix it on GitHub