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:
- Understand multipart form submissions
- Upload files using Express
- Use Multer middleware
- Store uploaded files
- Validate uploads
- Restrict file types
- Generate unique filenames
- Associate files with database records
- Display uploaded images
- Understand common upload security concerns
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:
- Form fields
- Files
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.
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.
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:
- Restrict file types
- Restrict size
- Generate filenames
- Store outside source code
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:
- Type
- Size
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:
- The
single.ejstemplate must be updated so that an array calledimagesthat contains the image filenames must be passed in. You can iterate on EJS using the following syntax:
<% images.forEach( image =>{ %>
<%= image.filename %>
<% }) %>
- The
productRepository.create()must be updated in order to support adding the image filenames into theproduct_imagestable.- A new repository method might be needed, in order to add the image filenames to the
product_imagestable.
- A new repository method might be needed, in order to add the image filenames to the
-
The images passed in from the form, are accessible via
req.fileswhich is an array. - The HTML form input must have the
multipleattribute set:
<input type="file" name="images" multiple>
- In order to get all the rows from a
SELECTSQL query, you must use theall()statement method:
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:
- Multipart forms
- Multer middleware
- File uploads
- Image management
- File validation
- Static file serving
- Filename generation
- Upload security
- Database file references
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.