Topic 14: Image Upload
📖 5 min read · 🎯 advanced · 🧭 Prerequisites: generating-signed-apk, react-forms
Why this matters
Here's the thing — almost every real-world app you'll build needs to handle images. A job portal needs profile photos. A product catalogue needs item pictures. A document tool needs scans. Up until now we've been sending plain JSON between React and Express, which is clean and simple. But an image is not JSON — it's binary data, and you have to package it differently. Today we wire together a React front-end and a Node/Express back-end to handle multipart/form-data uploads end to end, with multer doing the heavy lifting on the server. By the end, you'll know exactly how files travel from a browser to your backend.
What You'll Learn
- Bootstrap a React app and build a reusable
ImageUploadcomponent with file selection and live preview - Construct a
FormDatapayload and POST it asmultipart/form-datausingaxios - Configure an Express server with
multerdisk storage to receive, rename, and persist uploaded files - Handle CORS between the React dev server and the Express API
- Validate the full round-trip by running both processes together
The Analogy
Think of image uploading like dropping off a package at a post office. Your browser is the customer filling out the label (FormData) and handing over the parcel (the image binary). axios is the courier van that drives it to the post office (/upload endpoint). multer is the postal clerk who checks the package, stamps it with a unique timestamp, and places it in the correct storage bin (uploads/ folder). The response slip the clerk hands back is the JSON confirmation your React component logs to the console.
Chapter 1: Setting Up the React Application
Bootstrap a fresh Create React App project, then install axios for HTTP requests:
npx create-react-app image-upload-app
cd image-upload-app
npm install axios
npm start
The dev server starts on http://localhost:3000. Keep this terminal open — you will add a second terminal for the backend later.
Chapter 2: Creating the Image Upload Component
Create src/components/ImageUpload.js. This component tracks two pieces of state: the raw File object and a temporary object URL used for the inline preview.
import React, { useState } from 'react';
import axios from 'axios';
function ImageUpload() {
const [file, setFile] = useState(null);
const [preview, setPreview] = useState(null);
const handleFileChange = (event) => {
const selectedFile = event.target.files[0];
setFile(selectedFile);
setPreview(URL.createObjectURL(selectedFile));
};
const handleUpload = async () => {
if (!file) return;
const formData = new FormData();
formData.append('image', file);
try {
const response = await axios.post('http://localhost:5000/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
});
console.log('Upload successful:', response.data);
} catch (error) {
console.error('Error uploading the file:', error);
}
};
return (
<div>
<input type="file" onChange={handleFileChange} />
{preview && (
<img
src={preview}
alt="Preview"
style={{ maxWidth: '200px', marginTop: '10px' }}
/>
)}
<button onClick={handleUpload}>Upload</button>
</div>
);
}
export default ImageUpload;
Key mechanics:
URL.createObjectURL(selectedFile)generates a local blob URL so the preview renders instantly without a server round-trip.FormData.append('image', file)sets the field name thatmulterwill look for on the server (upload.single('image')).- Setting
Content-Type: multipart/form-dataexplicitly tellsaxios(and the browser) to encode the body as a multipart form payload.
Chapter 3: Integrating the Image Upload Component
Wire ImageUpload into the root App component:
src/App.js:
import React from 'react';
import './App.css';
import ImageUpload from './components/ImageUpload';
function App() {
return (
<div className="App">
<h1>Image Upload</h1>
<ImageUpload />
</div>
);
}
export default App;
Nothing more is needed on the React side — all upload logic lives inside ImageUpload.
Chapter 4: Setting Up the Backend Server
Create a backend/ directory alongside your React project (not inside src/):
mkdir backend
cd backend
npm init -y
npm install express multer
backend/package.json (after npm init and install):
{
"name": "backend",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"start": "node index.js"
},
"dependencies": {
"express": "^4.17.1",
"multer": "^1.4.3"
}
}
Create backend/uploads/ to hold incoming files:
mkdir backend/uploads
backend/index.js:
const express = require('express');
const multer = require('multer');
const path = require('path');
const app = express();
const PORT = 5000;
// Persist files to disk with a timestamp-based filename
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, 'uploads/');
},
filename: (req, file, cb) => {
// Prepend timestamp to preserve the original extension
cb(null, Date.now() + path.extname(file.originalname));
}
});
const upload = multer({ storage });
// Permissive CORS for local development
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
res.header('Access-Control-Allow-Headers', 'Content-Type');
next();
});
// Single-file upload endpoint — field name must match formData.append('image', ...)
app.post('/upload', upload.single('image'), (req, res) => {
try {
res.status(200).json({ message: 'Image uploaded successfully', file: req.file });
} catch (error) {
res.status(500).json({ message: 'Failed to upload image', error: error.message });
}
});
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});
Notable configuration decisions:
multer.diskStorage— gives you full control over destination folder and filename. The alternativemulter.memoryStorage()keeps the buffer in RAM, useful for streaming to cloud storage (S3, GCS) without touching disk.Date.now() + path.extname(file.originalname)— avoids filename collisions while keeping the original extension (.jpg,.png, etc.).- The CORS middleware uses a wildcard origin (
*) which is fine for local development. Tighten this to your actual front-end origin in production. upload.single('image')tells multer to handle exactly one file from the field namedimage. Useupload.array('images', 10)for multi-file uploads.
Start the backend:
npm start
# → Server is running on http://localhost:5000
Chapter 5: Testing the Image Upload Feature
With both servers running (localhost:3000 for React, localhost:5000 for Express), the full request/response flow looks like this:
sequenceDiagram
participant U as User (Browser)
participant R as React App :3000
participant E as Express API :5000
participant D as uploads/ directory
U->>R: Selects image file
R->>R: URL.createObjectURL → preview rendered
U->>R: Clicks "Upload"
R->>E: POST /upload (multipart/form-data)
E->>D: Saves file as <timestamp>.<ext>
E->>R: 200 { message, file: req.file }
R->>U: console.log(response.data)
Steps to verify end-to-end:
- Navigate to
http://localhost:3000 - Click the file input and choose any image
- Confirm the preview thumbnail appears below the input
- Click Upload
- Open DevTools → Console — you should see
Upload successful: { message: 'Image uploaded successfully', file: { ... } } - Inspect
backend/uploads/— the timestamped file should be present
🧪 Try It Yourself
Task: Extend the ImageUpload component to display the server's response message below the Upload button after a successful upload (or an error message on failure).
Success criterion: After clicking Upload, the text "Image uploaded successfully" (or "Error uploading the file: ...") appears in the UI — no need to open DevTools.
Starter snippet — add this state and JSX to ImageUpload.js:
const [statusMessage, setStatusMessage] = useState('');
// Inside handleUpload, replace the console.log line:
setStatusMessage(response.data.message);
// Inside catch:
setStatusMessage(`Error: ${error.message}`);
// In the return JSX, after the button:
{statusMessage && <p>{statusMessage}</p>}
🔍 Checkpoint Quiz
Q1. Why do we pass 'Content-Type': 'multipart/form-data' in the axios headers instead of application/json?
A) Because Express requires it for all POST requests
B) Because binary file data must be encoded as multipart form parts, not JSON
C) Because multer only works with JSON payloads
D) Because axios defaults to XML otherwise
Q2. Given this multer configuration:
filename: (req, file, cb) => {
cb(null, Date.now() + path.extname(file.originalname));
}
If a user uploads profile.PNG at Unix timestamp 1716300000000, what will the saved filename be?
A) profile.PNG
B) 1716300000000
C) 1716300000000.PNG
D) uploads/1716300000000.PNG
Q3. What is the purpose of URL.createObjectURL(selectedFile) in the handleFileChange handler?
A) It uploads the file to a CDN and returns the public URL
B) It generates a temporary in-browser blob URL for instant preview without a network request
C) It converts the file to base64 for JSON transmission
D) It validates that the selected file is a valid image
Q4. Your React app is deployed to https://app.example.com and your Express API to https://api.example.com. The upload fails with a CORS error. What is the minimal correct fix in backend/index.js?
A) Remove the CORS middleware entirely
B) Change '*' to 'https://app.example.com' in Access-Control-Allow-Origin
C) Add 'multipart/form-data' to the Access-Control-Allow-Methods header
D) Move the CORS middleware below the /upload route
A1. B — JSON cannot represent raw binary; multipart/form-data encodes each file as a named part with its own content type, which multer knows how to parse.
A2. C — Date.now() returns the numeric timestamp and path.extname returns .PNG (with the dot), so they concatenate to 1716300000000.PNG. The uploads/ directory is the destination, not part of filename.
A3. B — URL.createObjectURL creates a temporary blob: URL that points to the in-memory file data, enabling the <img> preview to render immediately without any server round-trip.
A4. B — The Access-Control-Allow-Origin value must match the exact origin of the requesting page. A wildcard (*) is rejected by browsers for credentialed requests on cross-origin production domains; specifying the exact origin resolves the CORS preflight.
🪞 Recap
- A
FormDataobject wraps binary file data soaxioscan POST it asmultipart/form-datato an Express endpoint. URL.createObjectURLgives instant client-side image previews without touching the server.multer.diskStoragelets you control both the destination folder and the saved filename;Date.now()prefix prevents collisions.upload.single('image')on the Express route must match the field name used informData.append('image', file)on the client.- CORS middleware must be registered before route handlers and should specify exact origins in production rather than
*.
📚 Further Reading
- multer on npm — official README covering disk storage, memory storage, file filtering, and size limits
- MDN: Using FormData Objects — the source of truth on constructing multipart payloads in the browser
- axios multipart/form-data guide — practical patterns for file upload progress tracking with
axiosonUploadProgress - ⬅️ Previous: React Forms
- ➡️ Next: Uploading App to Play Store for Android