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.
POST /api/images
Content-Type: application/octet-stream
Limit: 2 MiB encoded file
Accepted: decoded PNG or JPEG
GET /api/images/:id -> authorized download2. 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.
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.
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.
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.
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.
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.
DELETE /api/images/:id
1. Authenticate and authorize owner
2. Mark record for deletion
3. Remove bytes with retry support
4. Finalize metadata deletionIntegration 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.

