CodeKits.devNew developer offer

Image Uploads with React and Node.js

A practical walkthrough of image uploads with react and node.js, with code excerpts, failure checks and integration guidance.

By Jonathan · 5 min read

Published

Image Uploads with React and Node.js tutorial cover
In this article

This walkthrough covers Image Uploads with React and Node.js. An authenticated application, a private storage directory, an image decoder and a database table for ownership metadata. Scope this first implementation to PNG and JPEG images.

The video is an illustrated code walkthrough with AI narration. Code blocks are integration excerpts, not a complete tested application. Helper names such as stores, middleware and clients describe responsibilities you must implement in your existing project. No live server, payment provider or device execution is claimed.

Before you begin

Use a separate development environment, fictional data and test credentials. Record dependency versions and follow the official references below for your installed versions. Keep a working baseline and add one workflow at a time.

1. Define the upload contract

Define a narrow upload contract before building the form. This example accepts a small image body and returns a generated identifier. Other document types need their own validation and handling rules. Limit both encoded size and decoded image dimensions. Store ownership metadata separately from the file bytes so the download endpoint can enforce access without relying on a filename supplied by the client.

Define the upload contract
POST /api/images
Content-Type: application/octet-stream
Limit: 2 MiB encoded file
Accepted: decoded PNG or JPEG
GET /api/images/:id -> authorized download

2. Show upload progress

Use an upload mechanism that exposes progress when the interface needs it. This example sends the selected file as the request body and updates progress from computable upload events. Add separate success, server error, network error, timeout, and cancellation handlers. Completion of the byte transfer isn't the same as successful validation, so keep the interface pending until the server returns a successful response.

Show upload progress
const xhr = new XMLHttpRequest();
xhr.open("POST", "/api/images");
xhr.setRequestHeader("Content-Type", "application/octet-stream");
xhr.upload.onprogress = e => {
 if (e.lengthComputable) setProgress(e.loaded / e.total);
};
xhr.send(file);

3. Limit before decoding

Apply authentication, request forgery protection, and size limits before expensive decoding or storage. Configure compatible limits in the public proxy as well. A request parser limit doesn't bound every resource, so also enforce per-user quotas, request rates, and processing time. Return clear errors for unsupported content and excessive size without exposing internal paths or decoder implementation details.

Limit before decoding
app.post("/api/images", requireSession, requireAllowedOrigin,
 express.raw({type: "application/octet-stream", limit: "2mb"}),
 uploadImage);

4. Decode and re-encode the image

Don't trust the file extension or the content type claimed by the browser. Decode the input with a maintained image library, enforce a pixel limit, and restrict the supported formats. Re-encode the accepted image into your chosen output format. This also lets you normalize orientation and omit unnecessary metadata. Keep the decoder updated and consider isolated processing for higher-risk or higher-volume workloads.

Decode and re-encode the image
const input = sharp(req.body, {limitInputPixels: 16000000});
const metadata = await input.metadata();
if (!["png", "jpeg"].includes(metadata.format)) {
 return res.sendStatus(415);
}
const clean = await input.rotate().png().toBuffer();

5. Choose storage keys on the server

Generate the storage identifier on the server and keep the directory outside publicly served assets. Never join an arbitrary user filename into a filesystem path. Treat the original name as display metadata, with length and character validation. Use exclusive creation to avoid accidental overwrites. If saving the database metadata fails after writing the file, remove the orphaned file or queue a reliable cleanup operation.

Choose storage keys on the server
const id = randomBytes(32).toString("hex");
const target = path.join(privateRoot, id + ".png");
await writeFile(target, clean, {flag: "wx", mode: 0o600});
// Persist id, owner_id, output size and display name.

6. Authorize every download

A random-looking identifier is not an access-control rule. Resolve the metadata, check the authenticated user's permission, then return the stored output with a known content type. Define suitable cache behaviour for private files. If you later move to object storage, use short-lived authorized delivery links and keep the bucket private. The same permission checks still apply when the bytes live elsewhere.

Authorize every download
const image = await store.findImage(validatedId);
if (!image || image.ownerId !== req.user.id) {
 return res.sendStatus(404);
}
res.set("X-Content-Type-Options", "nosniff");
res.type("png").sendFile(imagePath);

7. Clean up deliberately

Deletion spans two systems when metadata and file bytes are stored separately. Design a recoverable sequence rather than assuming both operations always succeed together. A pending-deletion state and retryable job can make cleanup reliable. Exclude deleted records from downloads immediately. Periodically reconcile orphaned files and missing objects, and monitor storage quotas so abandoned uploads don't quietly consume all available space.

Clean up deliberately
DELETE /api/images/:id
1. Authenticate and authorize owner
2. Mark record for deletion
3. Remove bytes with retry support
4. Finalize metadata deletion

Integration checks

Test wrong type, corrupt images, oversized bodies, excessive dimensions, another owner’s identifier, cancelled uploads, failed metadata writes and failed file deletion.

These are verification instructions, not reported test results. The excerpts have not been executed as a complete integrated application. Review authentication, database schema, dependency versions and operational settings before deployment.

Official references

Explore CodeKits development services.