{"id":14494,"date":"2026-02-17T11:55:59","date_gmt":"2026-02-17T09:55:59","guid":{"rendered":"https:\/\/shortpixel.com/blog\/?p=14494"},"modified":"2026-02-18T11:23:24","modified_gmt":"2026-02-18T09:23:24","slug":"how-to-optimize-images-in-node-js-with-the-shortpixel-sdk","status":"publish","type":"post","link":"https:\/\/shortpixel.com\/blog\/how-to-optimize-images-in-node-js-with-the-shortpixel-sdk\/","title":{"rendered":"How to Optimize Images in Node.js with the ShortPixel SDK"},"content":{"rendered":"\n<p>Heavy images are one of the most common reasons websites feel sluggish. A product photo that weighs 3MB loads fine on your development machine with a fast connection, but it punishes real users on mobile, and it quietly tanks your Core Web Vitals scores while doing it.<\/p>\n\n\n\n<p>ShortPixel has been solving this problem for years, and the new Node.js SDK puts that optimization power directly into your backend code. Whether you&#8217;re building a content pipeline, processing user uploads, or compressing assets before they hit your CDN, the SDK fits cleanly into existing Node.js workflows.<\/p>\n\n\n\n<p>This guide walks through the SDK from installation to real-world patterns you can copy straight into a project.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">What the SDK actually does<\/h2>\n\n\n\n<p>At its core, the ShortPixel SDK is a wrapper around two API endpoints:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Reducer API<\/strong> \u2014 optimizes images fetched from remote URLs<\/li>\n\n\n\n<li><strong>Post-Reducer API<\/strong> \u2014 optimizes local files or buffers uploaded directly<\/li>\n<\/ul>\n\n\n\n<p>The SDK handles the transport details: polling, retries, response normalization.<\/p>\n\n\n\n<p>You pass in an image, configure how you want it processed, and get back download-ready URLs or write files locally.<\/p>\n\n\n\n<p>The real-world compression numbers can reach up to 86%, which makes a noticeable difference at scale.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Installation<\/h2>\n\n\n\n<pre class=\"wp-block-code\"><code>npm i @shortpixel-com\/shortpixel<\/code><\/pre>\n\n\n\n<p>One thing to be aware of before diving in: the package is ESM-only. Your package.json needs to declare this:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n  \"type\": \"module\"\n}<\/code><\/pre>\n\n\n\n<p>Node 20+ is recommended. The SDK won&#8217;t work reliably in CommonJS environments without additional configuration.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Setting up the client<\/h2>\n\n\n\n<p>Every operation flows through a ShortPixelClient instance. You&#8217;ll need an API key from shortpixel.com to get started.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import SHORTPIXEL from \"@shortpixel-com\/shortpixel\";\n\nconst { ShortPixelClient } = SHORTPIXEL;\n\nconst cli = new ShortPixelClient({\n  apiKey: process.env.SHORTPIXEL_API_KEY,\n});<\/code><\/pre>\n\n\n\n<p>Store your API key in an environment variable, never hardcode it. A .env file with a library like dotenv works fine here.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Four ways to think about the API<\/h2>\n\n\n\n<p>The SDK is designed with multiple layers of abstraction, and which one you reach for depends on how much control you need.<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Explicit helpers:<\/strong> <code>fromFile<\/code>, <code>fromUrl<\/code>, <code>fromBuffer<\/code>. These map directly to an endpoint and make it obvious what&#8217;s happening. Good for large codebases where clarity matters.<\/li>\n\n\n\n<li><strong>Intent-first aliases:<\/strong> <code>optimizeFile<\/code>, <code>optimizeUrl<\/code>, <code>optimizeBuffer<\/code>. Same behavior, naming that reads more naturally.<\/li>\n\n\n\n<li><strong>Generic dispatcher:<\/strong> <code>optimize(input)<\/code>. Pass in a file path, URL, or Buffer, and the SDK routes it automatically. Cleanest for quick scripts.<\/li>\n\n\n\n<li><strong>Feature helpers:<\/strong> <code>upscale<\/code>, <code>rescale<\/code>, <code>backgroundRemove<\/code>, <code>convert<\/code>. These set intelligent defaults for specific operations and still accept any additional options you need.<\/li>\n<\/ol>\n\n\n\n<p>In practice, you&#8217;ll likely mix these. Feature helpers are great for readability; explicit helpers are better when your team needs to trace exactly what&#8217;s hitting which endpoint.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Basic file optimization<\/h2>\n\n\n\n<p>The simplest case: take a local image, compress it, and write the result to a folder.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>const src = await cli.fromFile(\"assets\/product-shot.png\", { lossy: 1 });\nawait src.downloadTo(\".\/output\");<\/code><\/pre>\n\n\n\n<p>The <code>lossy<\/code> parameter controls compression aggressiveness:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>0<\/code> \u2014 lossless (no quality reduction)<\/li>\n\n\n\n<li><code>1<\/code> \u2014 lossy (good balance of size and quality)<\/li>\n\n\n\n<li><code>2<\/code> \u2014 glossy (more aggressive, still visually acceptable for most use cases)<\/li>\n<\/ul>\n\n\n\n<p>If you prefer named methods:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>const src = await cli.glossy(\"assets\/product-shot.png\");\nawait src.downloadTo(\".\/output\");<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Optimizing remote URLs<\/h2>\n\n\n\n<p>For images that live on a CDN or external server, pass the URL directly:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>const src = await cli.fromUrl(\n  \"https:\/\/example.com\/images\/hero-banner.jpg\",\n  { lossy: 1, convertto: \"+webp\" }\n);\nawait src.downloadTo(\".\/output\");<\/code><\/pre>\n\n\n\n<p>The <code>convertto: \"+webp\"<\/code> option tells ShortPixel to produce the optimized original plus a WebP version. If you only want WebP (no original), use <code>\"webp\"<\/code> without the <code>+<\/code>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Format conversion<\/h2>\n\n\n\n<p>Modern image formats like WebP and AVIF deliver significantly better compression than JPEG or PNG at equivalent quality. The <code>convertto<\/code> option handles this:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ Original + WebP\nawait cli.convert(\"assets\/banner.jpg\", \"+webp\");\n\n\/\/ Only WebP and AVIF, no original\nawait cli.convert(\"assets\/banner.jpg\", \"webp|avif\");\n\n\/\/ Original + WebP + AVIF\nawait cli.convert(\"assets\/banner.jpg\", \"+webp|+avif\");<\/code><\/pre>\n\n\n\n<p>The <code>+<\/code> prefix means &#8220;keep the optimized original as well.&#8221; Without it, only the converted formats are returned.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Resizing and upscaling<\/h2>\n\n\n\n<p>The SDK exposes several resize modes that correspond to different visual outcomes.<\/p>\n\n\n\n<p><strong>Upscaling<\/strong> \u2014 useful when you need to increase an image&#8217;s resolution without losing detail:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>const src = await cli.upscale(\"assets\/thumbnail.png\", 2, {\n  lossy: 0,\n  convertto: \"+webp\",\n});\nawait src.downloadTo(\".\/output\");<\/code><\/pre>\n\n\n\n<p>The second argument is the upscale factor (2x, 3x, or 4x). Valid values are <code>2<\/code>, <code>3<\/code>, and <code>4<\/code>.<\/p>\n\n\n\n<p><strong>Rescale (outer)<\/strong> \u2014 resize to fit within given dimensions, maintaining aspect ratio:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>const src = await cli.rescale(\"assets\/photo.jpg\", 1200, 800, {\n  convertto: \"+avif\",\n});\nawait src.downloadTo(\".\/output\");<\/code><\/pre>\n\n\n\n<p><strong>Resize inner<\/strong> \u2014 resize so the image fills the dimensions, potentially cropping:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>const src = await cli.resizeInner(\"assets\/product.jpg\", 600, 600);\nawait src.downloadTo(\".\/output\");<\/code><\/pre>\n\n\n\n<p><strong>Smart crop<\/strong> \u2014 like inner resize, but uses AI to focus on the most important part of the image:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>const src = await cli.smartCrop(\"assets\/portrait.jpg\", 400, 400, {\n  convertto: \"+webp\",\n});\nawait src.downloadTo(\".\/output\");<\/code><\/pre>\n\n\n\n<p>Smart crop is particularly useful for product images or portraits where you don&#8217;t want to lose the subject when cropping to a square.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Background removal and replacement<\/h2>\n\n\n\n<p>ShortPixel can remove image backgrounds or replace them with a color or another image.<\/p>\n\n\n\n<p>Remove the background (returns transparent PNG):<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>const src = await cli.backgroundRemove(\"assets\/product.png\", {\n  convertto: \"+webp\",\n});\nawait src.downloadTo(\".\/output\");<\/code><\/pre>\n\n\n\n<p>Replace with a solid color using an <code>#rrggbbaa<\/code> hex value (the last two characters control opacity):<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ Warning: background operations take longer to process\nconst src = await cli.backgroundChange(\"assets\/product.png\", \"#ffffff00\", {\n  lossy: 2,\n});\nawait src.downloadTo(\".\/output\");<\/code><\/pre>\n\n\n\n<p>Replace with another image:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>const src = await cli.backgroundChange(\n  \"assets\/product.png\",\n  \"https:\/\/example.com\/studio-background.jpg\",\n  { convertto: \"+webp\" }\n);\nawait src.downloadTo(\".\/output\");<\/code><\/pre>\n\n\n\n<p>Background operations are more computationally intensive than basic compression, so expect longer processing times. You may need to increase the polling configuration (more on that below).<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Processing multiple files<\/h2>\n\n\n\n<p>The SDK handles batches cleanly. You can pass arrays to any of the batch methods.<\/p>\n\n\n\n<p>Multiple local files:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>const src = await cli.fromFiles(&#91;\n  \"assets\/product-1.jpg\",\n  \"assets\/product-2.jpg\",\n  \"assets\/product-3.jpg\",\n], {\n  lossy: 1,\n  convertto: \"+webp\",\n});\n\nawait src.downloadTo(\".\/output\");<\/code><\/pre>\n\n\n\n<p>Multiple URLs:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>const src = await cli.fromUrls(&#91;\n  \"https:\/\/example.com\/images\/image-1.jpg\",\n  \"https:\/\/example.com\/images\/image-2.jpg\",\n], {\n  lossy: 2,\n  convertto: \"+avif\",\n});\n\nawait src.downloadTo(\".\/output\");<\/code><\/pre>\n\n\n\n<p>One important restriction: don&#8217;t mix local file paths and remote URLs in the same array. The SDK will throw an error because they route to different endpoints.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Working with buffers<\/h2>\n\n\n\n<p>When you&#8217;re handling file uploads in a web server and want to process images before saving them, buffers are the right approach:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import fs from \"fs\";\n\nconst imageBuffer = fs.readFileSync(\"assets\/upload.png\");\n\nconst src = await cli.fromBuffer(imageBuffer, \"upload.png\", {\n  lossy: 1,\n  convertto: \"+webp\",\n});\n\nawait src.downloadTo(\".\/output\");<\/code><\/pre>\n\n\n\n<p>For multiple buffers:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>const src = await cli.fromBuffers(&#91;\n  { buffer: bufferA, filename: \"image-a.png\" },\n  { buffer: bufferB, filename: \"image-b.png\" },\n], \"fallback.png\", { lossy: 1 });\n\nawait src.downloadTo(\".\/output\");<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Configuring polling and retries<\/h2>\n\n\n\n<p>ShortPixel&#8217;s API is asynchronous \u2014 you submit an image and poll for the result. The SDK handles this automatically, but the defaults may need adjusting for complex operations like background removal or AI-based upscaling.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ Increase timeout for long-running operations\ncli.set(\"timeout\", 60000);\n\n\/\/ More retries on temporary failures\ncli.set(\"retries\", 5);\ncli.set(\"retryDelay\", 1500);\n\n\/\/ Tune polling for complex operations\ncli.set(\"poll\", {\n  enabled: true,\n  interval: 3000,     \/\/ check every 3 seconds\n  maxAttempts: 30,    \/\/ try up to 30 times\n});<\/code><\/pre>\n\n\n\n<p>If you&#8217;re running background removal jobs and getting timeout errors, increasing <code>interval<\/code> and <code>maxAttempts<\/code> is usually the fix.<\/p>\n\n\n\n<p>You can also set a global default output format so you don&#8217;t have to repeat it on every call:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>cli.set(\"convertto\", \"+webp\");<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Inspecting metadata<\/h2>\n\n\n\n<p>Every Source object returned by an operation includes <code>lastMetas<\/code> \u2014 the raw API response for each processed item. This is useful for debugging or when you need to handle output URLs yourself:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>const src = await cli.fromFile(\"assets\/image.png\", {\n  lossy: 1,\n  convertto: \"+webp|+avif\",\n});\n\nfor (const meta of src.lastMetas || &#91;]) {\n  console.log(\"Status code:\", Number(meta?.Status?.Code));\n  console.log(\"Status message:\", meta?.Status?.Message);\n  console.log(\"Optimized URL:\", meta?.LossyURL);\n  console.log(\"WebP URL:\", meta?.WebPLossyURL || meta?.WebPLosslessURL);\n  console.log(\"AVIF URL:\", meta?.AVIFLossyURL || meta?.AVIFLosslessURL);\n}<\/code><\/pre>\n\n\n\n<p>Status code <code>2<\/code> means the image was processed successfully. Status code <code>1<\/code> means it&#8217;s still pending (the SDK handles polling automatically, so you shouldn&#8217;t see this often in normal operation).<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Error handling<\/h2>\n\n\n\n<p>The SDK uses typed errors, which makes it straightforward to handle specific failure modes:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>try {\n  const src = await cli.fromFile(\"assets\/image.png\", { upscale: 4 });\n  await src.downloadTo(\".\/output\");\n} catch (err) {\n  if (err.name === \"ShortPixelAuthError\") {\n    console.error(\"Invalid API key\");\n  } else if (err.name === \"ShortPixelQuotaError\") {\n    console.error(\"Monthly quota exceeded\");\n  } else if (err.name === \"ShortPixelTemporaryError\") {\n    console.error(\"Temporary API issue, retry later\");\n  } else if (err.name === \"ShortPixelInvalidRequestError\") {\n    console.error(\"Invalid request parameters\");\n  } else {\n    console.error(\"Unexpected error:\", err.name);\n    console.error(\"SP code:\", err.spCode);\n    console.error(\"Message:\", err.spMessage);\n  }\n}<\/code><\/pre>\n\n\n\n<p>The full set of typed errors the SDK can throw is: <code>ShortPixelError<\/code> (base class), <code>ShortPixelAuthError<\/code>, <code>ShortPixelQuotaError<\/code>, <code>ShortPixelTemporaryError<\/code>, <code>ShortPixelInvalidRequestError<\/code>, and <code>ShortPixelBatchError<\/code>.<\/p>\n\n\n\n<p>In batch mode, if one or more items fail, the SDK throws a <code>ShortPixelBatchError<\/code> that includes per-item error details. You can iterate through those to handle partial failures gracefully.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Example: Image pipeline for user uploads<\/h2>\n\n\n\n<p>Here&#8217;s a pattern you might use in a Node.js API route that processes user-uploaded images:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import SHORTPIXEL from \"@shortpixel-com\/shortpixel\";\nimport fs from \"fs\";\nimport path from \"path\";\n\nconst { ShortPixelClient } = SHORTPIXEL;\n\nconst cli = new ShortPixelClient({\n  apiKey: process.env.SHORTPIXEL_API_KEY,\n});\n\n\/\/ Set global defaults once\ncli.set(\"convertto\", \"+webp\");\ncli.set(\"poll\", { enabled: true, interval: 2000, maxAttempts: 25 });\n\nasync function processUserUpload(uploadedBuffer, originalFilename) {\n  try {\n    const src = await cli.fromBuffer(uploadedBuffer, originalFilename, {\n      lossy: 1,\n      keep_exif: 0,\n    });\n\n    const outputDir = path.join(\".\/storage\", \"optimized\");\n    fs.mkdirSync(outputDir, { recursive: true });\n\n    const files = await src.downloadTo(outputDir);\n\n    return {\n      success: true,\n      files,\n    };\n  } catch (err) {\n    console.error(\"Optimization failed:\", err.name, err.spMessage);\n    return {\n      success: false,\n      error: err.name,\n    };\n  }\n}<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">A few things to keep in mind<\/h2>\n\n\n\n<p>HTTPS is enforced throughout the SDK. Any input URL using plain <code>http:\/\/<\/code> will be rejected before a network call is made.<\/p>\n\n\n\n<p>Output URLs that come back over HTTP from the API are automatically upgraded to HTTPS. This is the expected behavior and not something you need to work around.<\/p>\n\n\n\n<p>The <code>pluginVersion<\/code> constructor option must be five characters or fewer. It&#8217;s optional, but if you pass something longer, initialization will fail.<\/p>\n\n\n\n<p><code>downloadTo()<\/code> only works after an optimization call completes. Calling it on a freshly instantiated Source without running a job first won&#8217;t do anything useful. useful.<\/p>\n\n\n\n<div class=\"wp-block-group has-palette-color-8-background-color has-background is-vertical is-content-justification-left is-layout-flex wp-container-core-group-is-layout-e21fc307 wp-block-group-is-layout-flex\" style=\"border-top-left-radius:0px;border-top-right-radius:20px;border-bottom-left-radius:0px;border-bottom-right-radius:20px;border-top-color:var(--wp--preset--color--palette-color-5);border-top-width:1px;border-right-color:var(--wp--preset--color--palette-color-5);border-right-width:1px;border-bottom-color:var(--wp--preset--color--palette-color-5);border-bottom-width:1px;border-left-color:var(--wp--preset--color--palette-color-4);border-left-width:5px;margin-top:var(--wp--preset--spacing--60);margin-bottom:var(--wp--preset--spacing--60);padding-top:var(--wp--preset--spacing--60);padding-right:var(--wp--preset--spacing--60);padding-bottom:var(--wp--preset--spacing--60);padding-left:var(--wp--preset--spacing--60)\">\n<p><strong>Resources<\/strong><\/p>\n\n\n\n<p>\ud83d\udcc4 <strong><a href=\"https:\/\/www.npmjs.com\/package\/@shortpixel-com\/shortpixel\" data-type=\"link\" data-id=\"https:\/\/www.npmjs.com\/package\/@shortpixel-com\/shortpixel\" target=\"_blank\" rel=\"noopener\">ShortPixel SDK<\/a><\/strong><br>\ud83d\udcc4 <strong><a href=\"https:\/\/shortpixel.com\/api-docs\" data-type=\"link\" data-id=\"https:\/\/shortpixel.com\/api-docs\">API documentation<\/a><\/strong><\/p>\n<\/div>\n","protected":false},"excerpt":{"rendered":"<p>Heavy images are one of the most common reasons websites feel sluggish. A product photo that weighs 3MB loads fine on your development machine with a fast connection, but it punishes real users on mobile, and it quietly tanks your Core Web Vitals scores while doing it. ShortPixel has been solving this problem for years, [&hellip;]<\/p>\n","protected":false},"author":22,"featured_media":14496,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-14494","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-world-of-wordpress"],"blocksy_meta":[],"_links":{"self":[{"href":"https:\/\/shortpixel.com\/blog\/wp-json\/wp\/v2\/posts\/14494","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/shortpixel.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/shortpixel.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/shortpixel.com\/blog\/wp-json\/wp\/v2\/users\/22"}],"replies":[{"embeddable":true,"href":"https:\/\/shortpixel.com\/blog\/wp-json\/wp\/v2\/comments?post=14494"}],"version-history":[{"count":18,"href":"https:\/\/shortpixel.com\/blog\/wp-json\/wp\/v2\/posts\/14494\/revisions"}],"predecessor-version":[{"id":14584,"href":"https:\/\/shortpixel.com\/blog\/wp-json\/wp\/v2\/posts\/14494\/revisions\/14584"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/shortpixel.com\/blog\/wp-json\/wp\/v2\/media\/14496"}],"wp:attachment":[{"href":"https:\/\/shortpixel.com\/blog\/wp-json\/wp\/v2\/media?parent=14494"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/shortpixel.com\/blog\/wp-json\/wp\/v2\/categories?post=14494"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/shortpixel.com\/blog\/wp-json\/wp\/v2\/tags?post=14494"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}