Topic 14 of 15 · React Developer

Topic 14 : Image Upload

Lesson TL;DRTopic 14: Image Upload 📖 5 min read · 🎯 Advanced · 🧭 Prerequisites: networking, reactforms Why this matters Here's the thing — at some point in almost every real project, a user needs to upload a p...
5 min read·advanced·image-upload · react · nodejs · express

Topic 14: Image Upload

📖 5 min read · 🎯 Advanced · 🧭 Prerequisites: networking, react-forms

Why this matters

Here's the thing — at some point in almost every real project, a user needs to upload a photo. A profile picture, a product image, a document scan. And that's when you realise: sending an image is completely different from sending text or JSON. You can't just put an image in a fetch call the usual way. You need FormData, you need the right headers, and on the server side you need something like multer to actually receive and save the file. In this lesson, we'll build both sides — a React frontend using axios, and a Node.js/Express backend — so you can see the full picture working together.

What You'll Learn

  • Bootstrap a React application and create a dedicated ImageUpload component
  • Use useState, URL.createObjectURL, and FormData to select and preview images before upload
  • POST multipart/form-data to a backend with axios
  • Set up an Express server with multer for disk-based file storage
  • Handle CORS and wire the full upload flow together

The Analogy

Think of image upload like dropping off a physical package at a post office. You — the React client — fill out a shipping label (FormData), stuff the item into a box (multipart/form-data encoding), and hand it to the clerk at the counter (POST /upload). The post office (Express + multer) checks the package, decides where to put it on the shelf (the uploads/ directory), stamps it with a unique name (timestamp + file extension), and hands you back a receipt (the JSON response). Neither side can do the job alone: the client prepares the parcel, the server stores it. Understanding both sides is what separates a complete implementation from a half-finished prototype.

Chapter 1: Setting Up the React Application

The class started with a fresh Create React App project.

npx create-react-app image-upload-app
cd image-upload-app
npm start

This gives you the standard React dev environment running at http://localhost:3000. The next step is building the component that handles file selection, preview, and upload.

Chapter 2: Creating the ImageUpload Component

the trainer introduced the ImageUpload component — the entire client-side story lives here.

src/components/ImageUpload.js:

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 at work here:

  • URL.createObjectURL(selectedFile) — creates a temporary in-browser URL for the selected file so you can render a <img> preview without touching the network at all.
  • FormData — the browser's native container for multipart/form-data payloads. formData.append('image', file) names the field image, which the backend will reference.
  • axios.post with 'Content-Type': 'multipart/form-data' — tells axios to serialize the FormData as a multipart body rather than JSON.

Chapter 3: Integrating the ImageUpload Component

The class dropped 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;

Clean and minimal — App stays focused on composition, while ImageUpload owns all the upload logic.

Chapter 4: Setting Up the Backend Server

With the React side ready, the trainer turned to the backend. The class created a separate backend/ directory alongside the React app.

backend/package.json:

{
  "name": "backend",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "start": "node index.js"
  },
  "dependencies": {
    "express": "^4.17.1",
    "multer": "^1.4.3"
  }
}

Install the dependencies:

cd backend
npm install express multer

backend/index.js:

const express = require('express');
const multer = require('multer');
const path = require('path');

const app = express();
const PORT = 5000;

// Configure storage for multer
const storage = multer.diskStorage({
    destination: (req, file, cb) => {
        cb(null, 'uploads/');
    },
    filename: (req, file, cb) => {
        cb(null, Date.now() + path.extname(file.originalname)); // Appending extension
    }
});

const upload = multer({ storage: storage });

// Middleware to handle CORS issues
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();
});

// Upload endpoint
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}`);
});

Breaking down the backend configuration:

  • multer.diskStorage — tells multer to write files to disk rather than holding them in memory.
  • destination — the callback specifies uploads/ as the target directory (must exist before files arrive).
  • filenameDate.now() + path.extname(file.originalname) generates a unique, collision-resistant name while preserving the original file extension.
  • upload.single('image') — middleware that processes exactly one file from the field named image — matching what the React FormData.append('image', file) sends.
  • CORS middleware — sets permissive Access-Control-Allow-* headers so the React dev server on port 3000 can POST to the Express server on port 5000 without being blocked by the browser.

Create the uploads directory, then start the server:

mkdir backend/uploads
npm start

The server will log: Server is running on http://localhost:5000.

sequenceDiagram
    participant U as User (Browser)
    participant R as React App (port 3000)
    participant E as Express Server (port 5000)
    participant D as uploads/ directory

    U->>R: Selects file via <input type="file">
    R->>R: URL.createObjectURL() → renders preview
    U->>R: Clicks "Upload"
    R->>R: Appends file to FormData
    R->>E: POST /upload (multipart/form-data)
    E->>E: multer processes file
    E->>D: Writes file as <timestamp>.<ext>
    E->>R: 200 JSON { message, file }
    R->>U: console.log("Upload successful")

Chapter 5: Testing the Image Upload Feature

With both servers running — the React dev server on port 3000 and the Express server on port 5000 — the class ran a full end-to-end test:

  1. Navigate to http://localhost:3000 in the browser.
  2. Click the file input and select any image file.
  3. A preview renders immediately below the input (via URL.createObjectURL).
  4. Click the Upload button.
  5. The browser console logs Upload successful: with the server's JSON response.
  6. Check backend/uploads/ — the file is stored there with a timestamp-based name.

Both halves must be running simultaneously. The React app serves the UI; the Express server handles storage. If CORS errors appear, verify the Access-Control-Allow-Origin: * middleware is in place before any route definitions.

🧪 Try It Yourself

Task: Extend the ImageUpload component to display the server's returned filename after a successful upload.

Success criterion: After clicking Upload, a paragraph below the button shows the text: Saved as: <filename> where <filename> is the value of response.data.file.filename.

Starter snippet — add this state and JSX to ImageUpload.js:

const [savedName, setSavedName] = useState(null);

// Inside handleUpload, after the axios call succeeds:
setSavedName(response.data.file.filename);

// In the return JSX, after the button:
{savedName && <p>Saved as: {savedName}</p>}

Run both servers, upload a file, and confirm the paragraph appears with the timestamped filename.

🔍 Checkpoint Quiz

Q1. Why does URL.createObjectURL(selectedFile) work for showing a preview without making a network request?

Q2. Given this multer filename callback:

filename: (req, file, cb) => {
    cb(null, Date.now() + path.extname(file.originalname));
}

If a user uploads photo.PNG at Unix timestamp 1716300000000, what will the stored filename be?

A) photo.PNG B) 1716300000000.PNG C) 1716300000000photo.PNG D) uploads/1716300000000.PNG

Q3. What is the purpose of setting 'Content-Type': 'multipart/form-data' in the axios request headers?

A) It enables CORS on the server B) It tells axios to serialize the body as JSON C) It signals to the server that the body is a multipart payload containing file data D) It is required by URL.createObjectURL

Q4. The upload endpoint returns a 200 but backend/uploads/ is empty. What is the most likely cause?

A1. URL.createObjectURL creates a temporary object URL that references the file already held in browser memory (from the <input> element). The image never leaves the device — the browser renders it directly from the in-memory File object.

A2. B) 1716300000000.PNGDate.now() produces the timestamp string and path.extname('photo.PNG') returns .PNG, concatenated together.

A3. C) It signals to the server that the body is a multipart payload containing file data — this is what causes the browser and axios to encode the FormData object as multipart/form-data rather than application/json or application/x-www-form-urlencoded.

A4. The uploads/ directory likely does not exist. Multer's diskStorage destination does not auto-create the target folder — mkdir backend/uploads must be run before the server starts, otherwise multer silently fails to write the file.

🪞 Recap

  • URL.createObjectURL generates an instant in-browser preview URL without any network round-trip.
  • FormData with append('image', file) is the correct way to send binary file data from a React component.
  • axios.post needs 'Content-Type': 'multipart/form-data' in headers so the payload is encoded correctly.
  • multer.diskStorage controls where and under what name uploaded files are written on the server.
  • The CORS middleware must be registered before route handlers so cross-origin requests from the React dev server are accepted.

📚 Further Reading

Like this topic? It’s one of 15 in React Developer.

Block your seat for ₹2,500 and join the next cohort.