⇦ Back

WDX-180

Web Development X

File Uploads & Image Management

Working with Real Files

Text is easy.

Files are where web applications start getting interesting.

So far, our CMS stores:

  Name
  Description
  Price

But real products also need:

  Images
  PDF Manuals
  Datasheets
  Downloads

Today we’ll learn how to upload files from a browser and store references to them in our database.

This is one of the first features that makes a CRUD application feel like a professional CMS.

Learning Objectives

By the end of this lesson, students will be able to:

Part 1 — Why File Uploads Are Different

Normal forms:

  <input name="name">

send:

  Text

File inputs:

  <input type="file" name="image">

send:

  Binary Data

The browser must use:

  multipart/form-data

instead of:

  application/x-www-form-urlencoded

Part 2 — Understanding multipart/form-data

Regular request:

  name=keyboard
  price=89.99

Multipart request:

  name=keyboard

  [file bytes]

  price=89.99

Contains both:


Express cannot process this by default.

We need middleware.

Part 3 — Introducing Multer

The most common Express upload middleware is:

Multer

Install:

  npm install multer

Import:

  // index.js
  const multer = require('multer');

Create upload middleware:

  const upload = multer({ dest: 'uploads/' });

Now uploaded files are stored in:

  uploads/

directory.

Part 4 — Creating the Upload Form

Edit product form:

  <form method="post" enctype="multipart/form-data">
      <input type="file" name="image">
      <button>
          Upload
      </button>
  </form>

Most common beginner mistake:

Forgetting:

  enctype="multipart/form-data"

Without it:

  File never arrives

Part 5 — Processing Uploads

Route:

  router.post('/edit/:id',
      // Multer Middleware for file uploading
      upload.single('image'),
      (req, res) => {

        // Contains information about the uploaded file
        // handled by the Multer middleware
        console.log(req.file);

      }
  );

Multer places uploaded file inside:

  req.file

Example:

  {
    filename:
      '5d8a7f6e9c3',

    originalname:
      'keyboard.jpg',

    mimetype:
      'image/jpeg',

    size:
      125000
  }

Understanding req.file

Useful properties:

Property Purpose
filename Generated name
originalname Original file
mimetype File type
size File size
path Stored path

Part 6 — Storing Image References

Do NOT store:

  Entire image

inside SQLite.

Store:

  Filename

instead.

Add column:

  ALTER TABLE products

  ADD COLUMN image TEXT;

Example value:

  5d8a7f6e9c3.jpg

Repository:

  UPDATE products

  SET image = ?

  WHERE id = ?

Database stores:

  Reference

not file contents.

Part 7 — Serving Uploaded Files

Files exist on disk.

Browser cannot access them yet.

Expose uploads folder:

  app.use('/uploads', express.static('uploads'));

Example:

  /uploads/image.jpg

becomes publicly accessible.

Displaying Images

View:

  <img src="/uploads/<%= product.image %>" alt="<%= product.name %>">

Result:

  Product Image

appears on page.

Part 8 — Generating Better Filenames

Default Multer names:

  7fa1c2f8b4...

Not ideal.

Custom storage:

  const storage = multer.diskStorage({
    destination: 'uploads/',
    filename: ( req, file, cb ) => {
      cb(
        null,
        Date.now() + '-' + file.originalname
      );
    }
  });
  const upload = multer({ storage });

Example:

  1712345678-keyboard.jpg

Much easier to debug.

Part 9 — Restricting File Types

Dangerous:

  virus.exe

Dangerous:

  shell.php

Accept only images via fileFilter.

Example:

  multer({
    // ...
    fileFilter:( req, file, cb) => {
        const allowed =
            [
                'image/jpeg',
                'image/png',
                'image/webp'
            ];
        cb(
            null,
            allowed.includes(
                file.mimetype
            )
        );

    }
  });

Only image uploads allowed.

Part 10 — Limiting File Size

Without limits:

  10GB upload

Possible.

Not ideal.

Limit.

  multer({
    // ...
    limits: {
      fileSize: 5 * 1024 * 1024
    }
  })

Equivalent:

  5 MB

Large enough for images.

Small enough to avoid abuse.

Part 11 — Handling Upload Errors

Example:

  File too large

Example:

  Wrong file type

Handle errors:

  const multer = require('multer');
  const upload = multer({
    limits: { ... }
    // ...
  })

  router.post("/edit", (req, res)=>{
    upload.single("image")(req, res, err =>{

      let error;

      if ( err instanceof multer.MulterError ){
        // Handle Multer error...
        error = "Invalid file";
      }
      // ...

      // Display errors:
      return res.render('products/edit',
          {
            title: "Edit Product",
            error,
          }
      );
    });
  });

Never fail silently.

Users should know what happened.

Part 12 — Replacing Existing Images

Current:

  Upload image

works.

But:

  Upload second image

leaves:

  Old file

on disk.

Result:

  Unused files accumulate

Future improvement:

  fs.unlink(...)

to remove old images.

We’ll revisit cleanup strategies later.

Part 13 — Security Considerations

Do you remember the mantra “Treat all user input as evil!”

❌ Never trust:

  Filename

❌ Never trust:

  File extension

❌ Never trust:

  MIME type alone

Real systems often inspect:

  Actual file contents

before accepting uploads.

For our CMS:

is sufficient.

Part 14 — Product Creation with Images

Current:

  Create Product

Enhanced:

  Create Product
  Upload Image

Single form:

  <input type="text" name="name">
  <input type="file" name="image">

Create product and image together.

This is how many CMS platforms operate.

Part 15 — Database Design Discussion

Current:

  products

  image

One image.

  erDiagram
    products {
      INTEGER id
      TEXT name
      TEXT description
      REAL price
      TEXT image
    }

Future:

  product_images

table.

Example:

  Product

  1

linked to:

  image1.jpg
  image2.jpg
  image3.jpg
  erDiagram
      products {
          integer id PK
          string name
          text description
      }
      product_images {
          integer id PK
          integer product_id FK
          string filename
      }
      products ||--o{ product_images : has

This becomes important for galleries.

We’ll keep one image for now.

Common Beginner Mistakes

Forgetting enctype

❌ Bad:

  <form method="post">

✅ Must be:

  multipart/form-data

Storing Images in SQLite

Usually unnecessary.

Store filenames instead.

No File Validation

Always validate:

Public Uploads Without Restrictions

Dangerous.

Never allow arbitrary uploads.

Ignoring Cleanup

Unused files eventually consume storage.

Bonus Challenge

Create:

  product_images

table.

Schema:

  CREATE TABLE product_images (

      id INTEGER PRIMARY KEY,

      product_id INTEGER,

      filename TEXT

  );

Allow:

  Multiple Images

per product.

Use:

  upload.array('images')

instead of:

  upload.single('image')

Display gallery:

  <img ...>
  <img ...>
  <img ...>

under product details.

Here are some of the required steps and things to keep in mind in order to make this work:

  <% images.forEach( image =>{ %>
    <%= image.filename %>
  <% }) %>
  <input type="file" name="images" multiple>
  const stmt = db.prepare(`
      SELECT *
      FROM product_images
      WHERE product_id = ?
    `);

  const result = stmt.all(id);  

Congratulations.

You’ve just crossed from simple CRUD into media management, one of the foundational capabilities of real-world CMS platforms.

Key Takeaways

Today you learned:

At this stage, the CMS can manage not only structured data but also media assets. This is a major step toward building applications that resemble WordPress, Shopify, Drupal, Ghost, and other production content management systems.


⚠️ A large part of the content of this module was created using Generative AI (ChatGPT). The synthetic (AI-generated) content was reviewed and curated by Kostas Minaidis.


Project maintained by in-tech-gration Hosted on GitHub Pages — Theme by mattgraham