Storing Uploaded Files and Serving Them in Express

javascript dev.to

yooo its me shivam

and today we are doing to talk about file uploads yessss

it might sound a piece of shit topic but it is hard okay so lets start 😭


first lets start with frontend and backend flow

frontend send the file to the backend

backend receives

thing to be handled:

  • taking the data properly (and when i say data i mean file)
  • after that store them properly
  • and in the end serve that data to the user properly

okay this is the main goal


first let understand one thing

and that is how the data will come from frontend to backend

Normal HTTP requests usually send:

{"name":"shivam"}
Enter fullscreen mode Exit fullscreen mode

This is easy.

But files are NOT text.

A PNG file is raw binary bytes:

89 50 4E 47 ...
Enter fullscreen mode Exit fullscreen mode

So browsers needed a special way to send everything in proper way:

  • text
  • files
  • metadata

all together in ONE request.

and that will be done using:

multipart/form-data

why called multipart?

because it contain multi form data 😭

yes this is why it is called like that


Example

Part 1 β†’ username

Part 2 β†’ image file

Part 3 β†’ another field

Each part is separated by using a special string called:

Boundary

like for example

When browser sends:

const form = new FormData();

form.append("username", "shivam");
form.append("image", file);
Enter fullscreen mode Exit fullscreen mode

The browser internally creates something like:

POST /upload HTTP/1.1
Content-Type: multipart/form-data; boundary=----WebKit123
Enter fullscreen mode Exit fullscreen mode

this is how the body look like

------WebKit123
Content-Disposition: form-data; name="username"

shivam

------WebKit123
Content-Disposition: form-data; name="image"; filename="cat.png"
Content-Type: image/png

<binary file bytes here>

------WebKit123--
Enter fullscreen mode Exit fullscreen mode

Now Let’s Break This Down

Part 1

------WebKit123
Content-Disposition: form-data; name="username"

shivam
Enter fullscreen mode Exit fullscreen mode

This means:

{
  username: "shivam"
}
Enter fullscreen mode Exit fullscreen mode

Part 2 (File)

------WebKit123
Content-Disposition: form-data; name="image"; filename="cat.png"
Content-Type: image/png
Enter fullscreen mode Exit fullscreen mode

This is metadata.

It tells:

  • field name = image
  • filename = cat.png
  • mime type = image/png

Then comes:

(binary bytes)
Enter fullscreen mode Exit fullscreen mode

Actual file data.


How Data Actually Travels

HTTP body travels as STREAMS

NOT like a giant packet.

Instead:

Chunk 1
Chunk 2
Chunk 3
Chunk 4
Enter fullscreen mode Exit fullscreen mode

Like:

------WebKit123
Content-Dispo
Enter fullscreen mode Exit fullscreen mode

then:

sition: form-data;
Enter fullscreen mode Exit fullscreen mode

then:

name="image"
Enter fullscreen mode Exit fullscreen mode

Everything arrives gradually.


Node.js Receives It as Stream

Inside Node.js req is actually a readable stream.

You can literally do:

req.on("data", chunk => {
   console.log(chunk);
});
Enter fullscreen mode Exit fullscreen mode

Example you may see:

<Buffer 2d 2d 2d 2d 57 65 62 4b>
Enter fullscreen mode Exit fullscreen mode

These are raw bytes.


now the best part comes

how a developer should handle this

see express is not having anything inbuilt to handle this

but there is one module:

multer

it helps us convert that raw bit into proper data

kinda like this:

{
  fieldname: 'image',
  originalname: 'cat.png',
  encoding: '7bit',
  mimetype: 'image/png',
  destination: 'uploads/',
  filename: 'a8f91c.png',
  path: 'uploads/a8f91c.png',
  size: 34567
}
Enter fullscreen mode Exit fullscreen mode

let see how multer does that

multer parse this stream manually like:

  • Reads chunks
  • Detects boundaries
  • Separates parts
  • Reads headers
  • Extracts metadata
  • Writes file bytes to disk

How File Is Recreated

Suppose multer extracts:

89 50 4E 47 ...
Enter fullscreen mode Exit fullscreen mode

(this data is after cleaning all the other data this is only the file data)

These are PNG bytes.

It simply writes them into a file using:

  • fs.writeFile()
  • or stream pipe

Because files ARE just bytes.

yes it is that simple 😭


SUPER IMPORTANT UNDERSTANDING

A file is NOT special.

A file is just:

Raw bytes + metadata
Enter fullscreen mode Exit fullscreen mode

Example:

cat.png

is just:

[bytes] stored on disk
Enter fullscreen mode Exit fullscreen mode

now this is how the multer code looks like

Backend Setup

const express = require("express");
const multer = require("multer");

const app = express();

const storage = multer.diskStorage({
  destination: function (req, file, cb) {
    cb(null, "uploads/");
  },

  filename: function (req, file, cb) {
    cb(null, Date.now() + "-" + file.originalname);
  }
});

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

app.post("/upload", upload.single("image"), (req, res) => {
  console.log(req.file);

  res.send("File uploaded");
});

app.listen(3000);
Enter fullscreen mode Exit fullscreen mode

Understanding upload.single("image")

This means:

  • expect ONE file
  • field name should be "image"

Frontend looks like this:

<input type="file" name="image" />
Enter fullscreen mode Exit fullscreen mode

must match exactly.


lets understand this code

first there is:

multer.diskStorage()
Enter fullscreen mode Exit fullscreen mode

this means what it says:

going to store on disk 😭

this code specify the destination

here it is /uploads folder

destination: function (req, file, cb) {
  cb(null, "uploads/");
}
Enter fullscreen mode Exit fullscreen mode

then comes filename

(just give a name to that file)

filename: function (req, file, cb) {
  cb(null, Date.now() + "-" + file.originalname);
}
Enter fullscreen mode Exit fullscreen mode

then passing that into multer function

and in the end using it as middleware like:

upload.single("image")
Enter fullscreen mode Exit fullscreen mode

req.file

and one more thing

in the next route handler u can access the file using:

req.file
Enter fullscreen mode Exit fullscreen mode

it contains:

{
  fieldname: 'image',
  originalname: 'cat.png',
  mimetype: 'image/png',
  destination: 'uploads/',
  filename: '8f91c.png',
  path: 'uploads/8f91c.png',
  size: 34567
}
Enter fullscreen mode Exit fullscreen mode

Multer creates this object manually.


everything in short

destination

Defines where files will be stored.

uploads/
Enter fullscreen mode Exit fullscreen mode

filename

Creates unique file names.

Example:

171523432-photo.png
Enter fullscreen mode Exit fullscreen mode

This prevents filename collisions.

Without unique names:

image.png
image.png
image.png
Enter fullscreen mode Exit fullscreen mode

Files would overwrite each other.


Creating Upload Middleware

const upload = multer({ storage });
Enter fullscreen mode Exit fullscreen mode

Upload Route

app.post("/upload", upload.single("image"), (req, res) => {
  res.send("File uploaded successfully");
});
Enter fullscreen mode Exit fullscreen mode

Understanding upload.single()

upload.single("image")
Enter fullscreen mode Exit fullscreen mode

This means:

  • Expect one file
  • Field name must be "image"

Frontend:

<input type="file" name="image" />
Enter fullscreen mode Exit fullscreen mode

Names must match.


Folder Structure After Upload

project/
β”‚
β”œβ”€β”€ uploads/
β”‚   β”œβ”€β”€ 171523432-profile.png
β”‚   └── 171523500-resume.pdf
β”‚
β”œβ”€β”€ server.js
└── package.json
Enter fullscreen mode Exit fullscreen mode

Visual Flow

Browser Request
       ↓
/uploads/photo.png
       ↓
Express Static Middleware
       ↓
Find File in uploads Folder
       ↓
Return File Response
Enter fullscreen mode Exit fullscreen mode

now one of the best parts

storing that file 😭

we have two option whenever it comes to storing any file

  1. Local Storage
  2. External Storage

1. Local Storage

Files are stored directly inside the server filesystem.

Example structure:

project/
β”‚
β”œβ”€β”€ uploads/
β”‚   β”œβ”€β”€ image1.png
β”‚   β”œβ”€β”€ profile.jpg
β”‚   └── resume.pdf
β”‚
β”œβ”€β”€ server.js
└── package.json
Enter fullscreen mode Exit fullscreen mode

The uploaded file physically exists inside the project folder.


2. External Storage

Instead of storing files on the server:

Files are stored in external services like:

  • AWS S3
  • Cloudinary
  • Firebase Storage
  • DigitalOcean Spaces

Flow:

User Upload
     ↓
Express Server
     ↓
Cloud Storage
Enter fullscreen mode Exit fullscreen mode

The Problem After Uploading

Now files exist on the server.

But users still cannot access them from browser.

Why?

Because Express does NOT expose folders automatically.


Serving Static Files in Express

To make uploaded files accessible:

Use:

express.static()
Enter fullscreen mode Exit fullscreen mode

Static File Serving

app.use("/uploads", express.static("uploads"));
Enter fullscreen mode Exit fullscreen mode

This line is extremely important.

It tells Express:

"Serve files inside uploads folder publicly"


How Static Serving Works

Suppose file exists:

uploads/photo.png
Enter fullscreen mode Exit fullscreen mode

Now browser can access:

http://localhost:3000/uploads/photo.png
Enter fullscreen mode Exit fullscreen mode

Express automatically serves the file.


Important Security Considerations

File uploads are dangerous if handled carelessly 😭

Never trust uploaded files blindly.


1. Validate File Types

Users may upload:

  • virus.exe
  • malware.js
  • dangerous.php

Instead of images.

Use file filtering.

Example:

const fileFilter = (req, file, cb) => {
  if (file.mimetype.startsWith("image")) {
    cb(null, true);
  } else {
    cb(new Error("Only images allowed"));
  }
};
Enter fullscreen mode Exit fullscreen mode

2. Limit File Size

Huge uploads can crash servers.

Example:

const upload = multer({
  storage,
  limits: {
    fileSize: 5 * 1024 * 1024
  }
});
Enter fullscreen mode Exit fullscreen mode

5MB limit.


3. Rename Uploaded Files

Never trust original filenames.

Bad:

myvirus.exe
Enter fullscreen mode Exit fullscreen mode

Good:

171523432-file.png
Enter fullscreen mode Exit fullscreen mode

4. Avoid Executable Uploads

Never allow:

  • .exe
  • .bat
  • .sh
  • .php

These may become security risks.


5. Store Sensitive Files Privately

Not all uploads should be public.

Examples:

  • Aadhaar cards
  • Passports
  • Medical documents

These should NOT use public static serving.

Instead:

  • Store privately
  • Add authentication before access

Public vs Private File Access

Public Access

/uploads/image.png
Enter fullscreen mode Exit fullscreen mode

Anyone with URL can access it.

Used for:

  • Product images
  • Profile photos
  • Blog thumbnails

Private Access

but only this it require is authorization 😭

Example flow:

User Login
    ↓
Authentication Check
    ↓
Server Sends File
Enter fullscreen mode Exit fullscreen mode

Used for sensitive documents.


final flow of working

High-Level Upload Flow

User Selects File
        ↓
Frontend Sends Multipart Request
        ↓
Multer Processes File
        ↓
File Stored in uploads/
        ↓
Express Static Middleware Exposes File
        ↓
Browser Accesses File via URL
Enter fullscreen mode Exit fullscreen mode

one thing i did not talk about

when we store data on external provider so how user will get that data?

best thing is:

when u upload user file to that provider that provider gives u a URL of that image

so u just need to send that to frontend

and that URL can be inside image tag

then u can see it 😭

so everything is done:

  • how to take the data
  • how to store it
  • how to give it back to the user

so i think this is the end

if u like the blog please let me know in comment

and like the blog

and subscribe me 😭

i have alot of knowledge i want to share

so if u want to be part of it then subscribe

happy happy bye reader

Source: dev.to

arrow_back Back to Tutorials