Code an express middleware to optimize your images

3 min. read

In this post we will implement an express.js middleware that utilizes the sharp package to optimize images.

TLDR; The full code is available as a gist at the end of this story.

To further speed up your REST API development in express. I suggest that you use the Express REST Snippets VS Code extenstion.

Initialization

mkdir express-image-opt-middleware
cd express-image-opt-middleware
npm init -y
touch index.js
touch utils.js

Now, let’s install our dependencies:

npm i express sharp
npm i -D nodemon

Configure npm scripts:

// package.json
{
  ...,
  "scripts": {
    "dev": "nodemon index.js",
    "start": "node index.js"
  },
  ...
}

Initialize an express server:

// index.js

const express = require('express');
const fs = require('fs/promises');
const path = require('path');

const app = express();

app.listen(8080, () => {
  console.log("server running at http://localhost:8080");
});

Normally in order to serve you static assets through express you use the built-in express.static middleware:

app.use(express.static('public'));

We don’t need this middleware because we are going to implement our own. The first thing to understand is that when you try to display an image, there is a GET request with a specific URL behind the scenes fetching the image from the server and displaying it. What we do here is intercept this request and serve an optimized image on-demand, based on the provided query parameters in the URL.

// index.js

app.get('*', async (req, res, next) => {
  const storagePath = path.join(__dirname, 'public');
  const fileName = req.params[0] ?? '';
  const filePath = path.join(storagePath, fileName);

  try {
    const stats = await fs.stat(filePath);
    if (!stats.isFile()) return next();

    // * the rest of the code goes here
  } catch(err) {
    console.error(err);
    res.status(404).send('File not found');
  }
});

We need to set a proper Content-Type header based on the requested image:

// utils.js
const path = require('path');

exports.getContentType = fileName => {
  const ext = path.extname(fileName).slice(1);
  let contentType;
  
  switch (ext) {
    case 'jpg':
    case 'jfif':
    case 'jpeg':
      contentType = 'image/jpeg';
      break;
    case 'png':
      contentType = 'image/png';
      break;
    case 'webp':
      contentType = 'image/webp';
      break;
    case 'svg':
      contentType = 'image/svg+xml';
      break;
    default:
      contentType = 'application/octet-stream';
  }

  return contentType;
};
// index.js

// * setting the Content-Type
// * make sure to require getContentType
const contentType = getContentType(fileName);
res.setHeader('Content-Type', contentType);
  
// * serve svg and unknown files as they are
if (['image/svg+xml', 'application/octet-stream'].includes(contentType)) {
  const readStream = createReadStream(filePath);
  readStream.pipe(res);
  readStream.on('error', next);
  return;
}

Note that the fs/promises API does not provide a createReadStream function. To get the best of both worlds, use:

const { createReadStream } = require('fs');
const fs = require('fs/promises');

This middleware understands the following query params:

Note that all of these params are optional.

If only one of w or h is specified, the image’s original aspect ratio will be used to calculate the other dimension.

// index.js

// * make sure to include the sharp package
const image = sharp(filePath);
const metadata = await image.metadata();
const aspectRatio = metadata.width / metadata.height;
const quality = Math.trunc(+(req.query.q ?? 100));
let width = Math.trunc(+(req.query.w ?? 0));
let height = Math.trunc(+(req.query.h ?? 0));

// * only width is specified
if (width && !height) {
  height = Math.round(width * (1 / aspectRatio));

  // * only height is specified
} else if (height && !width) {
  width = Math.round(height * aspectRatio);

  // * neither one is specified
} else {
  width = metadata.width;
  height = metadata.height;
}

const stream = image
  .resize({ width, height })
  .jpeg({ quality, progressive: true, force: false })
  .webp({ quality, progressive: true, force: false })
  .png({ quality, progressive: true, force: false });

stream.pipe(res);
stream.on('error', next);

Here’s the full code of the middleware as a gist: