Tutorial

How to Generate a Thumbnail on Upload

Generate a thumbnail on upload with sharp so the UI has a preview instantly, and the original and thumbnail are stored in one atomic operation.

On this page

  1. Introduction
  2. Pipe through sharp
  3. Set output format and size
  4. Store both versions
  5. Handle non-images
  6. Clean up on failure
  7. Test thumbnail size
  8. Common pitfalls
  9. Conclusion

Introduction

The hardest part of generate preview image is not writing the code — it is deciding what the code is allowed to reject. A sharp setup that accepts everything is easy to ship and expensive to operate, because every malformed input becomes a production surprise. The discipline is to enumerate the shapes the system actually receives, encode them in sharp, and let the library reject the rest loudly. This keeps the happy path fast and the failure path visible.

The cost of generate preview image without sharp is paid in incidents, not in code. The code looks fine because the happy path works, and the happy path is what you demo. The failure path is what you operate, and the failure path is where sharp earns its keep. Instrument the rejections, alert on the spikes, and let the metric tell you when the guard is misaligned with reality.

The sections below walk through the concrete decisions: where to put the sharp check, how to keep it fast under load, what to do when the contract changes, and how to test the failure paths so they degrade on purpose instead of by accident.

Pipe through sharp

Pipe through sharp is the first decision because it determines what the rest of the sharp pipeline can assume. A wrong choice here turns into generate preview image under load, and the invariant you thought sharp guaranteed no longer holds. Measure the impact before committing, because the cost of a bad choice is paid in incidents, not in a code review.

The test for Pipe through sharp is the one most teams skip: drive sharp to the failing condition and assert the system degrades gracefully. Write it with a fake clock for time-based logic, a stub for the failure path, and an assertion on the user-visible outcome. A test that passes by timing out is a hope, not a test.

import sharp from 'sharp';
async function thumb(input, out) {
  await sharp(input).resize(200, 200, { fit: 'cover' }).jpeg({ quality: 80 }).toFile(out);
}

The javascript above is the shape to ship for generate preview image. It is small enough to audit and specific enough that the failure behavior is obvious. Adapt the constants to your traffic and latency budget, and resist the urge to handle cases the system has not seen yet.

Set output format and size

The mechanism behind Set output format and size is straightforward in sharp, but the consequences ripple. A wrong choice here turns into generate preview image under load, and the invariant you thought sharp guaranteed no longer holds. Measure the impact before committing, because the cost of a bad choice is paid in incidents, not in a code review.

The test for Set output format and size is the one most teams skip: drive sharp to the failing condition and assert the system degrades gracefully. Write it with a fake clock for time-based logic, a stub for the failure path, and an assertion on the user-visible outcome. A test that passes by timing out is a hope, not a test.

Store both versions

Store both versions is where most sharp implementations leak performance. A wrong choice here turns into generate preview image under load, and the invariant you thought sharp guaranteed no longer holds. Measure the impact before committing, because the cost of a bad choice is paid in incidents, not in a code review.

The test for Store both versions is the one most teams skip: drive sharp to the failing condition and assert the system degrades gracefully. Write it with a fake clock for time-based logic, a stub for the failure path, and an assertion on the user-visible outcome. A test that passes by timing out is a hope, not a test.

test('thumbnail is 200x200', async () => {
  await thumb('in.png', 'out.jpg');
  const m = await sharp('out.jpg').metadata();
  expect(m.width).toBe(200);
});

Pin the behavior with this test. A sharp setup that passes this test degrades on purpose instead of by accident, which is the difference between a managed failure and an incident.

Handle non-images

The hard part of Handle non-images is not the happy path — it is what happens when sharp fails halfway. A wrong choice here turns into generate preview image under load, and the invariant you thought sharp guaranteed no longer holds. Measure the impact before committing, because the cost of a bad choice is paid in incidents, not in a code review.

The test for Handle non-images is the one most teams skip: drive sharp to the failing condition and assert the system degrades gracefully. Write it with a fake clock for time-based logic, a stub for the failure path, and an assertion on the user-visible outcome. A test that passes by timing out is a hope, not a test.

Clean up on failure

Clean up on failure should be observable without a debugger. A wrong choice here turns into generate preview image under load, and the invariant you thought sharp guaranteed no longer holds. Measure the impact before committing, because the cost of a bad choice is paid in incidents, not in a code review.

The test for Clean up on failure is the one most teams skip: drive sharp to the failing condition and assert the system degrades gracefully. Write it with a fake clock for time-based logic, a stub for the failure path, and an assertion on the user-visible outcome. A test that passes by timing out is a hope, not a test.

Test thumbnail size

Test thumbnail size is a trade, not a winner. A wrong choice here turns into generate preview image under load, and the invariant you thought sharp guaranteed no longer holds. Measure the impact before committing, because the cost of a bad choice is paid in incidents, not in a code review.

The test for Test thumbnail size is the one most teams skip: drive sharp to the failing condition and assert the system degrades gracefully. Write it with a fake clock for time-based logic, a stub for the failure path, and an assertion on the user-visible outcome. A test that passes by timing out is a hope, not a test.

Common pitfalls

The common pitfalls in generate preview image all reduce to one mistake: treating sharp as a formality instead of a contract. The contract is the thing the rest of the system relies on, and a weak contract propagates weakness everywhere.

  • Skipping the sharp boundary so generate preview image enters unvalidated.
  • Assuming sharp is fast without measuring the tail latency under load.
  • Letting generate preview image fail silently instead of failing loudly with a metric.
  • Testing only the happy path so the sharp failure surfaces in production.
  • Sharing mutable state across requests so one bad input poisons the next.

Conclusion

Do not conflate the conclusion with the implementation. They look similar from the outside but the failure modes differ: sharp fails fast and the caller knows, while an unchecked path fails slow and the caller guesses. Choose the one whose failure mode your system can absorb, and document the choice at the call site.

Self-check: write a test that drives sharp to the failing condition and assert the system degrades gracefully. Then generate preview image is a property you maintain, not a feature you hope for.

Sources: sharp, libvips

FAQ

Sync or async?
Async. sharp is libvips-backed and non-blocking. Do not block the event loop.
What format?
JPEG for photos, PNG for transparency, WebP if you can serve it.

Related Examples