ShortPixelExpress is the Express middleware layer for the ShortPixel Node SDK.
It scans incoming requests for images, sends each supported input to the right ShortPixel API flow, and gives your route handler a request that already contains optimized buffers, rewritten file paths, and normalized result metadata.
If you want the full client API (ShortPixelClient, fromFile, optimize, upscale, polling config, typed errors, and so on), see the repository root README and the official SDK guide:
- What the SDK actually does
- Setting up the client
- Four ways to think about the API
- Basic file optimization
- Optimizing remote URLs
- Format conversion
- Resizing and upscaling
- Background removal and replacement
- Processing multiple files
- Working with buffers
- Configuring polling and retries
- Inspecting metadata
- Error handling
- Example: Image pipeline for user uploads
- A few things to keep in mind
At its core, ShortPixelExpress is a request adapter over the SDK client:
- Uploaded buffers go through
fromBuffer(...) - Uploaded temp files or disk files go through
fromFile(...) - Remote body URLs go through
fromUrl(...)orfromUrls(...) - Local body paths go through
fromFile(...)orfromFiles(...)
The SDK still handles polling, retries, HTTPS validation, and response normalization.
The middleware handles the Express-specific part:
- reading
req.file,req.files, andreq.body - deciding whether each input is an upload, a remote URL, or a local path
- filtering fields through a whitelist / blacklist
- mutating uploaded files and local path fields with optimized outputs
- attaching normalized results to
req.shortPixel
In practice, you mount it before your route handler, then read req.shortPixel or use the already-mutated req.file, req.files, or req.body.
npm i @shortpixel-com/shortpixelThe package is ESM-only, so your app should use:
{
"type": "module"
}Node 20+ is the safest target.
ShortPixelExpress is exported from the package root:
import { ShortPixelExpress } from "@shortpixel-com/shortpixel";Every middleware instance needs a valid ShortPixel API key:
const optimizeImages = ShortPixelExpress({
apiKey: process.env.SHORTPIXEL_API_KEY,
lossy: 1,
convertto: "+webp",
});One important detail: the middleware does not parse the request body for you.
Mount the relevant parsers first:
express.json()orexpress.urlencoded()for body URL / path fieldsmulter,express-fileupload, or another multipart parser for uploaded files
If apiKey is missing, middleware creation fails immediately because the underlying SDK client requires it.
The normal Express flow looks like this:
request
-> body / multipart parser
-> ShortPixelExpress(...)
-> detect supported image inputs
-> optimize them through the SDK
-> mutate request data when needed
-> attach req.shortPixel
-> your route handlerThat means downstream handlers do not need to manually call downloadToBuffer() or decide which ShortPixel endpoint to hit. The middleware already did that.
The middleware reads uploaded files from:
req.filereq.filesas an arrayreq.filesas a keyed object
It works with common upload parser shapes such as:
bufferordatapathortempFilePathoriginalname,name, orfilenamesize
If the upload lives in memory, the middleware optimizes it into a buffer and replaces the file contents in place.
If the upload lives on disk, the middleware writes the optimized bytes back to disk and updates the file metadata on the request object.
Basic upload example:
import express from "express";
import multer from "multer";
import { ShortPixelExpress } from "@shortpixel-com/shortpixel";
const app = express();
const upload = multer({ storage: multer.memoryStorage() });
app.post(
"/upload",
upload.single("image"),
ShortPixelExpress({
apiKey: process.env.SHORTPIXEL_API_KEY,
lossy: 1,
convertto: "+webp",
}),
(req, res) => {
const result = req.shortPixel?.files?.[0];
res.json({
optimizedFilename: req.file?.originalname,
optimizedBytes: req.file?.buffer?.length,
resultFilename: result?.filename,
savedToRequest: !!req.file?.buffer,
});
}
);Any whitelisted body value that is a non-empty string and parses as a URL is treated as a remote image URL.
Arrays are supported too:
req.body.url = "https://example.com/hero.jpg";
req.body.urls = [
"https://example.com/a.jpg",
"https://example.com/b.jpg",
];The middleware optimizes those inputs and stores the results in req.shortPixel.urls.
The original body value is not replaced.
Example:
import express from "express";
import { ShortPixelExpress } from "@shortpixel-com/shortpixel";
const app = express();
app.use(express.json());
app.post(
"/from-url",
ShortPixelExpress({
apiKey: process.env.SHORTPIXEL_API_KEY,
lossy: 1,
convertto: "+avif",
}),
(req, res) => {
const result = req.shortPixel?.urls?.[0];
res.json({
sourceUrl: result?.input,
filename: result?.filename,
optimizedBytes: result?.buffer?.length,
});
}
);Remote URLs must still use https://.
http:// inputs are detected as URLs by the middleware, but the SDK rejects them before sending the request upstream.
Any other whitelisted non-empty body string is treated as a local filesystem path.
That includes single values and arrays:
req.body.imagePath = "/tmp/cover.png";
req.body.imagePaths = ["/tmp/a.png", "/tmp/b.png"];Each path is optimized and written next to the original file.
The output filename format is:
original-name.ext -> original-name.optimized.new-extAfter that, the middleware replaces the body value with the new optimized path.
Example:
import express from "express";
import { ShortPixelExpress } from "@shortpixel-com/shortpixel";
const app = express();
app.use(express.json());
app.post(
"/from-path",
ShortPixelExpress({
apiKey: process.env.SHORTPIXEL_API_KEY,
lossy: 1,
convertto: "webp",
extraWhitelist: ["coverPath"],
}),
(req, res) => {
const result = req.shortPixel?.paths?.[0];
res.json({
originalPath: result?.input,
optimizedPath: req.body.coverPath,
filename: result?.filename,
});
}
);When the request contains at least one supported, whitelisted input, the middleware initializes:
req.shortPixel = {
files: [],
urls: [],
paths: [],
};Each entry in those arrays has this shape:
{
kind: "file" | "url" | "path",
input: any,
field: string | null,
index: number | null,
source: {
kind: "file" | "url" | "path",
field: string | null,
index: number | null,
value: any,
name: string | null,
path: string | null
},
output: {
filename: string | null,
path: string | null,
size: number | null
},
buffer: Buffer,
meta: any,
filename: string
}The most useful properties in practice are:
result.buffer: optimized bytesresult.filename: output filename returned by ShortPixelresult.meta: raw SDK metadata for that itemresult.output.path: written file path when the middleware saved something to diskresult.field: which request field matchedresult.index: array position if the input came from an array
Example:
const fileResult = req.shortPixel?.files?.[0];
console.log(fileResult.kind); // "file"
console.log(fileResult.field); // "image"
console.log(fileResult.filename); // e.g. "photo.webp"
console.log(fileResult.buffer.length);
console.log(fileResult.meta);The same resolved whitelist is used for all supported input kinds:
- uploaded file field names
- body fields containing remote URLs
- body fields containing local paths
The built-in default field names are:
- URL keys:
url,urls,imageUrl,imageUrls - Path keys:
filepath,filepaths,filePath,filePaths,imagePath,imagePaths - File keys:
file,files,image,images,upload,uploads
You can customize that behavior with three middleware options:
ShortPixelExpress({
apiKey,
extraWhitelist,
overrideWhitelist,
blacklist,
...shortPixelOptions
});Resolution order is explicit:
- Start from the default whitelist, or
overrideWhitelistif you provide it. - Merge in
extraWhitelist. - Remove any keys listed in
blacklist.
Examples:
ShortPixelExpress({
apiKey: process.env.SHORTPIXEL_API_KEY,
extraWhitelist: ["cover", "heroUrl"],
});ShortPixelExpress({
apiKey: process.env.SHORTPIXEL_API_KEY,
overrideWhitelist: ["cover", "gallery"],
blacklist: ["gallery"],
});The second example only accepts cover.
One practical warning: for body values, any whitelisted non-URL string is treated as a local path. Do not whitelist generic text fields unless they really contain image paths.
The middleware adds five Express-facing options on top of the forwarded SDK options:
apiKey: requiredpassthrough: defaultfalseextraWhitelist: additional accepted field namesoverrideWhitelist: replaces the defaults entirelyblacklist: removes fields after whitelist resolution
Everything else is forwarded to the underlying SDK calls.
That means you can pass normal ShortPixel API options directly on the middleware:
lossyconverttoresizeresize_widthresize_heightupscale- background removal / replacement options
- and other options supported by the main SDK
Useful client-side references for those forwarded options:
- Format conversion
- Resizing and upscaling
- Background removal and replacement
- Configuring polling and retries
- Inspecting metadata
- Error handling
Example:
ShortPixelExpress({
apiKey: process.env.SHORTPIXEL_API_KEY,
lossy: 2,
convertto: "+webp|+avif",
keep_exif: 0,
});Uploaded files are mutated in place.
For memory uploads:
file.bufferbecomes the optimized bufferfile.dataalso becomes the optimized buffer when presentfile.sizeis updatedfile.originalnameis updated to the returned filenamefile.nameandfile.filenameare updated when presentfile.mimetypeis recalculated from the returned filename
For disk-backed uploads:
- the optimized bytes are written back to disk
- the file can be renamed if the output extension changes
file.path,file.tempFilePath, andfile.filenameare updated when present
Local path inputs are written to a sibling output file and the matching body field is replaced with that new path.
Remote URL inputs are not written back into req.body.
They only appear in req.shortPixel.urls.
If you want to mount ShortPixelExpress once at app level, use passthrough: true.
That lets unmatched requests continue without error, which is useful when only some routes carry images.
Example:
import express from "express";
import multer from "multer";
import { ShortPixelExpress } from "@shortpixel-com/shortpixel";
const app = express();
const upload = multer({ storage: multer.memoryStorage() });
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use((req, res, next) => {
if (!req.is?.("multipart/form-data")) {
next();
return;
}
upload.any()(req, res, next);
});
app.use(
ShortPixelExpress({
apiKey: process.env.SHORTPIXEL_API_KEY,
passthrough: true,
lossy: 1,
convertto: "+webp",
})
);This pattern works well when you want one central optimization layer instead of adding the middleware route by route.
The middleware always calls next(error) on failure.
That includes:
- SDK auth, quota, temporary, or invalid-request errors
- filesystem errors when reading local paths or writing optimized outputs
- invalid HTTPS inputs rejected by the SDK
- requests that contain files or body data, but nothing matches the resolved whitelist
A standard Express error handler is enough:
app.use((error, req, res, next) => {
res.status(500).json({
error: error.name,
message: error.message,
});
});One nuance matters here:
- If a request is completely empty, the middleware just calls
next(). - If a request contains body data or uploaded files but none of the field names match the whitelist, the middleware throws unless
passthroughistrue.
The whitelist error message includes:
- the accepted keys
- the received body keys
- the uploaded file field names found on the request
Uploads and local file paths smaller than 50_000 bytes are skipped.
That means:
- they do not throw
- they are not added to
req.shortPixel - uploaded file objects stay unchanged
- local body paths stay unchanged
Remote URLs are not filtered by that size rule.
If you need one sentence to remember the middleware:
ShortPixelExpress is the "inspect the request, optimize whatever matches, then hand me normalized results on req" layer on top of the SDK.
Use it when your app already lives in Express and you want image optimization to feel like normal middleware rather than manual client orchestration in every route.