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.
Consider the following flow:
Let's see how to implement it step-by-step.
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.
Let's create a createThumbnail() function that will take the following image:
A creates 3 different sizes rounded versions (100x100, 200x200, 400x400):
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}
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,})
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.
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 })}) // [...]
First, create an application on Defer using GitHub and configure a DEFER_TOKEN on your API hosting. Then, you're good to go!
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:
CRONs are an essential part of web applications, from automatic invoicing, triggering weekly notifications, or triggering search reindex or data backups. This article covers the CRONs best practices (windowing, monitoring, alerting) and an extensive list of deployment and hosting solutions.
This blog post is the first of the Inside Defer Series, opening a door into the inner workings of Defer on the technical and operating side. With hundreds of builds per day and continuous executions, introducing changes to the Defer Builder could be a slow and hazardous process.
Configuring Retries for your background function is an essential feature, as many use cases are subject to the randomness of functions' execution. This article covers three use cases along with three distinct retry strategies.