Build an image resize API

A complete Node.js service that accepts an uploaded image, resizes it, and returns a compact WebP — built on Express, multer, and sharp, with the full code and every run step.

Thumbnails, avatars, banners — almost every app needs to shrink images at some point, and shipping full-size uploads is slow and wasteful. This guide builds a small HTTP service that takes an image, resizes it to the dimensions you ask for, and hands back a compact WebP. It runs on the Node.js application and uses sharp, the fast image library whose prebuilt binaries install cleanly on Falix.

At a glance
You need A server running the Node.js application
Build with Express, multer (uploads), sharp (resizing)
Plan Any — free runs while your session timer has time, premium runs 24/7
Time About twenty minutes

New to the Node application? Read Node.js on Falix first — it covers the index.js entry file, automatic npm install, and the SERVER_PORT rule this project relies on.

What you're building

Endpoint Method Does
/ GET The upload form in public/
/resize?w=&h= POST Takes an uploaded image, returns a resized WebP

The resizer keeps aspect ratio, never upscales past the original, and caps the requested size so nobody can ask for a 40000×40000 monster.

Step 1 — Create the project files

You need three files. In the File Manager, create package.json:

{
  "name": "image-resizer",
  "version": "1.0.0",
  "private": true,
  "dependencies": {
    "express": "^4.19.2",
    "multer": "^1.4.5-lts.1",
    "sharp": "^0.33.4"
  }
}

Then index.js — the whole service:

const express = require('express');
const multer = require('multer');
const sharp = require('sharp');
const path = require('node:path');

const app = express();
const upload = multer({
  limits: { fileSize: 10 * 1024 * 1024 }, // 10 MB cap
  storage: multer.memoryStorage(),
});

app.use(express.static(path.join(__dirname, 'public')));

// POST an image file (field name "image") plus ?w= and/or ?h=.
// Returns a resized WebP.
app.post('/resize', upload.single('image'), async (req, res) => {
  if (!req.file) return res.status(400).json({ error: 'No image uploaded (field name must be "image").' });

  const width = clampDim(req.query.w);
  const height = clampDim(req.query.h);
  if (!width && !height) return res.status(400).json({ error: 'Pass at least ?w= or ?h=.' });

  try {
    const output = await sharp(req.file.buffer)
      .rotate()                                   // honour EXIF orientation
      .resize({ width, height, fit: 'inside', withoutEnlargement: true })
      .webp({ quality: 80 })
      .toBuffer();
    res.type('image/webp').send(output);
  } catch (err) {
    console.error('resize failed:', err.message);
    res.status(422).json({ error: 'Could not process that file as an image.' });
  }
});

function clampDim(v) {
  const n = parseInt(v, 10);
  if (Number.isNaN(n)) return undefined;
  return Math.min(Math.max(n, 1), 4000); // 1..4000 px
}

const PORT = process.env.SERVER_PORT || 8080;
app.listen(PORT, '0.0.0.0', () => console.log(`Listening on port ${PORT}`));

Finally a tiny front end at public/index.html so you can test in a browser:

<!DOCTYPE html>
<html lang="en">
<head><meta charset="utf-8"><title>Image resizer</title></head>
<body>
  <h1>Resize an image</h1>
  <form action="/resize?w=300" method="post" enctype="multipart/form-data">
    <input type="file" name="image" accept="image/*" required>
    <button>Resize to 300px wide</button>
  </form>
</body>
</html>

Step 2 — Start it

Press Start on the Console page. Because a package.json exists, Falix runs npm install automatically before your code — you'll watch Express, multer, and sharp download, then:

Listening on port 25565

That line flips your server to online. sharp ships prebuilt binaries for the server's Linux environment, so there's no compiler step and no native-build failure to worry about — it just lands and loads.

🎯 Good to know: multer.memoryStorage() keeps each upload in RAM and passes it straight to sharp — nothing is written to disk. That's simpler and avoids leaving stray files behind, at the cost of holding the file in memory while it's processed. The 10 MB fileSize limit keeps that bounded.

Step 3 — Try it

Open your server's address (from the Network page) in a browser, pick an image, and submit. The response is a resized WebP. Or drive it from a terminal:

curl -F "[email protected]" "http://YOUR_ADDRESS:PORT/resize?w=200" -o small.webp

Ask for width only (?w=200) and height scales to match; ask for both and the image fits inside that box without distortion. A 600×400 source requested at w=200 comes back 200×133 — aspect ratio preserved.

How the resize works

The pipeline is four chained calls on the sharp object:

Call Why
.rotate() Reads the photo's EXIF orientation flag and bakes it in, so phone photos aren't sideways
.resize({ width, height, fit: 'inside', withoutEnlargement: true }) fit: 'inside' keeps aspect ratio within your box; withoutEnlargement refuses to blow a small image up
.webp({ quality: 80 }) Encodes to WebP — much smaller than JPEG or PNG at the same quality
.toBuffer() Runs the pipeline and returns the bytes, which you send()

Want PNG or JPEG out instead? Swap .webp(...) for .png() or .jpeg({ quality: 80 }) and change the res.type(...) to match. Everything else beyond these Falix-shaped parts — crops, blurs, watermarks, format detection — is standard sharp; the official docs at sharp.pixelplumbing.com take it from here.

Extend it: save resized copies

Right now the resizer is stateless — it processes and forgets. To keep thumbnails, write the buffer to disk with sharp(...).toFile('thumbs/name.webp') instead of .toBuffer(), and serve the thumbs/ folder with another express.static. One caveat worth knowing: files you write live in /home/container, and a reinstall (or switching the server's application) wipes them. For anything you can't lose, store the bytes in a managed database or re-generate on demand.

Troubleshooting

  • Page won't load / connection refused — a port or bind-address problem, not a code one. The listen call must use SERVER_PORT and 0.0.0.0; see I can't reach my app.
  • Cannot find module 'sharp' — the install didn't finish. Scroll to the top of the console and read the npm install output; or open the Packages page and install sharp there, then restart.
  • HTTP 422 "Could not process that file" — the upload wasn't a real image (or was a format sharp doesn't read). That's sharp refusing bad input, exactly as intended.
  • HTTP 400 on submit — either no file reached the server (the form field must be named image) or you didn't pass ?w= or ?h=.
  • Killed under load — big images plus in-memory storage use RAM; the free plan shares 2.5 GB. See Out of memory.

Next steps

Was this guide helpful?