Topic 14 of 56 · Full Stack Advanced

Topic 14 : Image upload

Lesson TL;DRTopic 14: Image Upload 📖 5 min read · 🎯 advanced · 🧭 Prerequisites: generatingsignedapk, reactforms Why this matters Here's the thing — almost every realworld app you'll build needs to handle image...
5 min read·advanced·react · nodejs · express · multer

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 ImageUpload component with file selection and live preview
  • Construct a FormData payload and POST it as multipart/form-data using axios
  • Configure an Express server with multer disk 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 that multer will look for on the server (upload.single('image')).
  • Setting Content-Type: multipart/form-data explicitly tells axios (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 alternative multer.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 named image. Use upload.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:

  1. Navigate to http://localhost:3000
  2. Click the file input and choose any image
  3. Confirm the preview thumbnail appears below the input
  4. Click Upload
  5. Open DevTools → Console — you should see Upload successful: { message: 'Image uploaded successfully', file: { ... } }
  6. 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 FormData object wraps binary file data so axios can POST it as multipart/form-data to an Express endpoint.
  • URL.createObjectURL gives instant client-side image previews without touching the server.
  • multer.diskStorage lets 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 in formData.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

Like this topic? It’s one of 56 in Full Stack Advanced.

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