Menu
Engineering
July 6th, 2023

How to reliably process images in your Express application

Charly PolyCharly PolyCEO

Processing images directly from your Express endpoints comes with many downsides:

 

Impact on your overall API performance

sharp, while being efficient, is performing CPU-intensive operations that, by running on the same servers as your API web processes, will impact the overall performance of your API (ex: lowering RPS).

 

Does the API consumer need to get the result?

The sole duty of an API is to get requests that perform actions and may return an associated result. For image processing use cases, does the consumer needs the result? If not, why wait for the image processing to be performed before responding?

 

Memory leaks issues

Web processes running your API usually run for a long time, only rebooted on new deployments on your application. sharp memory leak issues are again resurfacing, as reported by many users.

Moving the image processing in the background (e.g., queues) is the best practice to ensure the reliability of your Express application.

 

Let's see an example that generates thumbnail variation during a new user onboarding flow.

 

 

Reliably create avatar thumbnails variation

Consider the following flow:

  1. The user uploads an avatar that is uploaded to S3 from the front-end
  2. Users submit their account details to POST /user along with the S3 file key as avatar
  3. The express POST /user endpoint:
    1. creates the user record with the proper avatar, and defaults thumbnails_* values
    2. Enqueue a background job to create the thumbnail variations without impacting the overall API performances
  4. The background job:
    1. creates the thumbnail variations using sharp: small, medium, and large rounded versions of the original avatar
    2. upload them on S3
    3. and updates the user record

 

Let's see how to implement it step-by-step.

 

 

1. The POST /user endpoint

Our POST /account endpoint creates a new account and sets default values for the thumbnails:

import express from "express"
import prisma from "./prismaClient"
const app = express()app.use(express.json())
const port = 3000
app.post("/user", async (req, res) => {  const data = req.body  const user = await prisma.user.create({    data: {      ...data,      avatar: data.fileName,      // Put placeholder values      thumbnailSmall: fileName,      thumbnailMedium: fileName,      thumbnailLarge: fileName,    },  })
  // TODO: create the thumbnail variations  //       in the background
  res.json({ user })})
app.listen(port, () => {  console.log(`Example app listening on port ${port}`)})

Let's now implement the thumbnail variations generation with sharp.

 

 

2. Implementing the image processing logic

Let's create a createThumbnail() function that will take the following image:

avatar

A creates 3 different sizes rounded versions (100x100, 200x200, 400x400):

rounded thumbnail

 

 

Creating rounded thumbnails with sharp

 

A rounded PNG version of an image is achieved by using the .composite() API from sharp with an SVG circle, used as a mask (blend: "dest-in"):

import sharp from "sharp"import {  S3Client,  GetObjectCommand,  PutObjectCommand,} from "@aws-sdk/client-s3"
import s3Config from "../s3Config"
const s3Client = new S3Client(s3Config)
const rect = (w: number, h: number) =>  Buffer.from(    `<svg>      <rect        x="0"        y="0"        width="${w}"        height="${h}"        rx="${w}"        ry="${h}"      />    </svg>`  )
const sizes = {  small: [100, 100],  medium: [200, 200],  large: [400, 400],} as const
async function createThumbnail(  fileName: string,  imageSource: Uint8Array,  size: keyof typeof sizes) {  const dimensions = sizes[size]  const thumbnailFile = `${fileName}_${size}`  const thumbnailSource = await sharp(imageSource)    .resize(...dimensions)    .composite([      {        input: rect(dimensions[0], dimensions[1]),        blend: "dest-in",      },    ])    .png()    .toBuffer()  const bucketParams = {    Bucket: process.env.AWS_S3_BUCKET_NAME,    Key: fileName,    Body: thumbnailSource,  }  await s3Client.send(new PutObjectCommand(bucketParams))  return thumbnailFile}

 

💡  imageSource as a Uint8Array

The imageSource argument is the content returned from the S3 GetObjectCommand(), used to retrieve the original avatar image from S3.

 

Let's now call our createThumbnail() function from a background function that will generate all the thumbnail variations from the original uploaded avatar.

 

 

Implementing the background function — using Defer

 

Let's create a background function that will run in the background - on Defer upon call.

For this, we create a src/defer/createThumbnails.ts file as follows:

import { defer } from "@defer/client"import {  S3Client,  GetObjectCommand,  PutObjectCommand,} from "@aws-sdk/client-s3"
import prisma from "../prismaClient"import s3Config from "../s3Config"import createThumbnail from "../createThumbnail"
const s3Client = new S3Client(s3Config)
//async function createThumbnails(accountId: string) {  const { avatar } = await prisma.user.findUnique({    where: {      id: accountId,    },  })
  const bucketParams = {    Bucket: process.env.AWS_S3_BUCKET_NAME,    Key: avatar,  }  const { Body } = await s3Client.send(new GetObjectCommand(bucketParams))  const source = await Body?.transformToByteArray()  if (!source) {    throw new Error("could not parse image source")  }
  const [thumbnailSmall, thumbnailMedium, thumbnailLarge] =    await Promise.all([      createThumbnail(avatar, source, "small"),      createThumbnail(avatar, source, "medium"),      createThumbnail(avatar, source, "large"),    ])
  await prisma.user.update({    where: {      id: accountId,    },    data: {      thumbnailSmall,      thumbnailMedium,      thumbnailLarge,    },  })}
export default defer(createThumbnails, {  // Retry in case of AWS or database issues  retry: 5,})

 

💡  Background function

A background function is an async function defined, wrapped with defer(), and exported as default from a file within the defer/ directory.

By doing so, all invocations of a background function will trigger a run on Defer, offloading your Express API.

 

Let's now see how to trigger a createThumbnails() execution from our POST /user endpoint.

 

 

3. Triggering a background function run

Let's import createThumbnails() from defer/createThumbnails and simply call the background function with the newly created user.id.

 

src/server.ts

// [...]import createThumbnails from "../defer/createThumbnails"
app.post("/user", async (req, res) => {  const user = await prisma.user.create({    data: {      // ...      avatar: fileName,      // put placeholder values      thumbnailSmall: fileName,      thumbnailMedium: fileName,      thumbnailLarge: fileName,    },  })
  // this call with offload the function  //  execution on the Defer Platform  await createThumbnails(user.id)
  res.json({ user })})
// [...]

 

💡  How to deploy my background functions

First, create an application on Defer using GitHub and configure a DEFER_TOKEN on your API hosting. Then, you're good to go!

 

 

Going further

Moving the image processing outside of the API lifecycle is one example of how to build resilient and performant Express APIs.


Other use cases to explore are:

  • Generating reports or export of data
  • Interacting with third-party APIs (ex: Stripe)
  • Sending and receiving webhooks
Join the community and learn how to get started, and provide feedback.
Stay tuned about our latest product and company updates.
Start creating background jobs in minutes.
Copyright ©2024 Defer Inc.