Build a Secure File Sharing App with Node.js | A Complete Guide

Learn to build a full-stack, secure file sharing application using Node.js, Express, Multer, and MongoDB. Step-by-step tutorial with code examples, best practices, and deployment tips.

Build a Secure File Sharing App with Node.js | A Complete Guide
Build Your Own Secure File Sharing App with Node.js: A Complete Guide
Ever emailed a file to yourself because it was too big for Slack? Or struggled to send a video to a friend, only to be blocked by your email provider's attachment limits? We've all been there. The demand for simple, quick, and secure file sharing is everywhere, powering tools like WeTransfer, Google Drive shared links, and Dropbox.
But what if you could build your own?
In this comprehensive guide, we're not just going to talk about file sharing; we're going to roll up our sleeves and build a fully functional, secure file sharing web application from scratch using Node.js. This isn't just a coding exercise—it's a journey into core web development concepts that will level up your skills. Think of it as building your own mini WeTransfer clone.
By the end of this tutorial, you'll have a web app that allows users to:
Upload single or multiple files.
Generate a unique, shareable download link.
Automatically clean up files after a set period (e.g., 24 hours).
And the best part? You'll understand every line of code that makes it tick. This project is a fantastic way to solidify your understanding of back-end logic, database interactions, and security practices. If you're looking to transition from beginner to a professional developer, building projects like this is key. To learn professional software development courses such as Python Programming, Full Stack Development, and MERN Stack, visit and enroll today at codercrafter.in.
What We'll Be Using: Our Tech Stack
Before we dive into the code, let's get familiar with the tools and libraries that will power our application.
Node.js & Express.js: The heart of our server. Node.js allows us to use JavaScript on the server-side, and Express.js is a minimal and flexible web application framework that provides a robust set of features.
Multer: A Node.js middleware for handling
multipart/form-data
, which is primarily used for uploading files. It's the workhorse that will process the files sent from our web form.MongoDB & Mongoose: We need a database to store information about our uploaded files (like the original filename, the generated unique link, and the expiration time). MongoDB is a NoSQL database that fits perfectly for this, and Mongoose is an elegant ODM (Object Data Modeling) library for MongoDB and Node.js.
EJS (Embedded JavaScript templating): A simple templating language that lets us generate HTML markup with plain JavaScript. It will power our front-end views.
Bcryptjs: For hashing sensitive information. We'll use it to optionally add password protection to our files.
Nodemailer: A module for Node.js to send emails. We can use this to email the download link to recipients directly from our app.
Step 1: Laying the Foundation - Project Setup
First, ensure you have Node.js and MongoDB installed on your system. You can download them from their official websites.
Let's create our project directory and initialize it.
bash
mkdir node-file-sharer
cd node-file-sharer
npm init -y
This creates a package.json
file that will track our dependencies.
Now, let's install the necessary packages:
bash
npm install express multer mongoose ejs bcryptjs nodemailer
npm install --save-dev nodemon
nodemon
is a development tool that automatically restarts our server when it detects file changes.
Next, create the basic structure of our project:
text
node-file-sharer/
│
├── models/
│ └── File.js
├── routes/
│ └── files.js
├── public/
│ ├── css/
│ └── js/
├── views/
├── uploads/
├── app.js
└── .env
Update your package.json
to add a start script and a dev script:
json
"scripts": {
"start": "node app.js",
"dev": "nodemon app.js"
}
Step 2: Building the Server - app.js
This is the main entry point of our application. Let's build it step by step.
javascript
// app.js
require('dotenv').config(); // Load environment variables from .env file
const express = require('express');
const mongoose = require('mongoose');
const fileRoutes = require('./routes/files');
const path = require('path');
const app = express();
const PORT = process.env.PORT || 3000;
// Connect to MongoDB
mongoose.connect(process.env.DATABASE_URL, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
const db = mongoose.connection;
db.on('error', console.error.bind(console, 'MongoDB connection error:'));
db.once('open', () => {
console.log('Connected to Database');
});
// Middleware
app.set('view engine', 'ejs'); // Set EJS as the templating engine
app.use(express.static('public')); // Serve static files from the 'public' directory
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Routes
app.use('/api/files', fileRoutes); // All our API routes will be prefixed with /api/files
// Basic route for the home page
app.get('/', (req, res) => {
res.render('index'); // This will render views/index.ejs
});
// Start the server
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
Create a .env
file to store your environment variables securely:
bash
# .env
DATABASE_URL=mongodb://localhost:27017/file-sharer
PORT=3000
Step 3: Defining Our Data Model - models/File.js
What information do we need to store for each uploaded file?
The original filename.
The path to the file on our server (or in cloud storage).
A unique identifier for the download link.
A password (hashed, for security).
The download count (to track popularity).
An expiration date.
Let's translate this into a Mongoose Schema.
javascript
// models/File.js
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const fileSchema = new mongoose.Schema({
filename: {
type: String,
required: true,
},
path: {
type: String,
required: true,
},
originalName: {
type: String,
required: true,
},
password: String,
downloadCount: {
type: Number,
default: 0,
},
expiresAt: {
type: Date,
default: () => new Date(+new Date() + 24 * 60 * 60 * 1000), // Default to 24 hours from now
},
});
// Hash the password before saving the file document, but only if it's modified
fileSchema.pre('save', async function (next) {
if (!this.isModified('password')) {
return next();
}
this.password = await bcrypt.hash(this.password, 12);
next();
});
// Instance method to check if the password is correct
fileSchema.methods.correctPassword = async function (candidatePassword, userPassword) {
return await bcrypt.compare(candidatePassword, userPassword);
};
// Static method to find non-expired files
fileSchema.statics.findUnexpired = function () {
return this.find({ expiresAt: { $gt: new Date() } });
};
module.exports = mongoose.model('File', fileSchema);
Step 4: Handling File Uploads with Multer - routes/files.js
This is where the magic happens. We'll create routes for uploading files and generating links.
javascript
// routes/files.js
const express = require('express');
const multer = require('multer');
const File = require('../models/File');
const router = express.Router();
const { v4: uuidv4 } = require('uuid'); // For generating unique IDs
// Configure Multer for file uploads
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, 'uploads/'); // Files will be saved in the 'uploads' directory
},
filename: (req, file, cb) => {
const uniqueName = `${Date.now()}-${Math.round(Math.random() * 1E9)}-${file.originalname}`;
cb(null, uniqueName);
},
});
const upload = multer({
storage: storage,
limits: { fileSize: 1000000 * 100 }, // Limit file size to 100MB
}).single('myfile'); // 'myfile' is the name of the file field in the form
// @desc Handle file upload
// @route POST /api/files
router.post('/', (req, res) => {
// Store file with Multer
upload(req, res, async (err) => {
// Validate request
if (!req.file) {
return res.status(400).json({ error: 'Please upload a file.' });
}
if (err) {
return res.status(500).send({ error: err.message });
}
// Store file data into Database
const file = new File({
filename: req.file.filename,
path: req.file.path,
originalName: req.file.originalname,
});
try {
const savedFile = await file.save();
// Respond with a link for the user to share
res.render('download', {
downloadLink: `${process.env.APP_BASE_URL}/api/files/${savedFile._id}`,
fileId: savedFile._id,
});
} catch (error) {
res.status(500).send({ error: 'Could not save file to database.' });
}
});
});
module.exports = router;
We need to update our .env
to include the application's base URL.
bash
# .env
APP_BASE_URL=http://localhost:3000
Step 5: Creating the Download Logic
Now, we need to handle the download requests when someone uses the generated link.
Add this route to your routes/files.js
:
javascript
// @desc Handle file download
// @route GET /api/files/:id
router.get('/:id', async (req, res) => {
try {
const file = await File.findById(req.params.id);
if (!file) {
return res.render('error', { error: 'File not found.' });
}
// Check if the file has expired
if (file.expiresAt < new Date()) {
return res.render('error', { error: 'This download link has expired.' });
}
// Increment the download count
file.downloadCount++;
await file.save();
// Trigger the file download
res.download(file.path, file.originalName);
} catch (error) {
return res.render('error', { error: 'Something went wrong.' });
}
});
Step 6: Crafting the User Interface with EJS
Our server logic is solid, but users need a way to interact with it. Let's create simple views.
views/index.ejs
(The Upload Page)
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>FileSharer - Share files easily</title>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<div class="container">
<h1>Share Files Securely</h1>
<p>Upload a file and get a shareable link. It's that simple.</p>
<form action="/api/files" method="POST" enctype="multipart/form-data">
<div class="file-input-container">
<input type="file" name="myfile" id="file" class="file-input" required>
<label for="file" class="file-label">Choose a file...</label>
</div>
<button type="submit" class="btn">Upload & Get Link</button>
</form>
<p class="promo">Want to build complex, real-world applications like this from the ground up? <strong>To learn professional software development courses such as Python Programming, Full Stack Development, and MERN Stack, visit and enroll today at <a href="https://codercrafter.in" target="_blank">codercrafter.in</a>.</strong></p>
</div>
<script src="/js/script.js"></script>
</body>
</html>
views/download.ejs
(The Success Page)
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Download Link Ready!</title>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<div class="container">
<h1>Your file is ready to share!</h1>
<p>Copy the link below and send it to anyone you want. The link will expire in 24 hours.</p>
<div class="link-container">
<input type="text" id="downloadLink" value="<%= downloadLink %>" readonly>
<button onclick="copyToClipboard()" class="btn-copy">Copy</button>
</div>
<p class="success-message" id="message"></p>
<a href="/" class="btn">Upload Another File</a>
</div>
<script>
function copyToClipboard() {
const copyText = document.getElementById("downloadLink");
copyText.select();
copyText.setSelectionRange(0, 99999); // For mobile devices
navigator.clipboard.writeText(copyText.value);
const message = document.getElementById("message");
message.textContent = "Link copied to clipboard!";
}
</script>
</body>
</html>
Step 7: Adding Security and Advanced Features
A basic app is great, but a professional one needs security and polish.
1. Password Protection:
Modify the upload route to accept a password. Then, create a new route /api/files/:id/password
to handle password verification before allowing the download.
2. Email the Link:
Integrate Nodemailer to send the download link directly to an email address provided by the user.
3. Scheduled Cleanup:
Use a library like node-cron
to run a job every night that deletes all files where expiresAt
is less than the current date. This prevents your server from filling up with old files.
javascript
// utils/cron.js
const cron = require('node-cron');
const File = require('../models/File');
const fs = require('fs');
// Run every day at 2 AM
cron.schedule('0 2 * * *', async () => {
console.log('Running file cleanup job...');
try {
const expiredFiles = await File.find({ expiresAt: { $lt: new Date() } });
expiredFiles.forEach(async (file) => {
// Delete the file from the filesystem
fs.unlink(file.path, (err) => {
if (err) console.error(`Error deleting file ${file.path}:`, err);
});
// Delete the record from the database
await File.findByIdAndDelete(file._id);
});
console.log(`Cleaned up ${expiredFiles.length} expired files.`);
} catch (error) {
console.error('Error in cleanup job:', error);
}
});
Remember to require this file in your app.js
: require('./utils/cron');
Real-World Use Cases & Best Practices
Building this app teaches you skills that are directly applicable in the industry.
Use Case 1: Internal Company Tool. Companies often need to share large design assets, video files, or database dumps internally. A self-hosted solution can be more secure and cost-effective.
Use Case 2: Client Portals. Freelancers and agencies can use a customized version of this to deliver large final project files (like video edits or website backups) to their clients.
Use Case 3: Educational Platforms. A platform could use similar logic to allow students to submit their project files and for teachers to download them.
Best Practices We've Implemented:
Security: Hashing passwords with
bcryptjs
, validating file types and sizes with Multer, and using environment variables for sensitive data.Scalability: By storing file metadata in MongoDB, we can easily query and manage files without scanning the filesystem. For massive scale, you'd replace the local
uploads
folder with cloud storage like AWS S3.Maintainability: Separating concerns into Models, Views, and Controllers (MVC) makes the code easier to read, test, and extend.
User Experience (UX): Providing immediate feedback with the download link, a copy-to-clipboard button, and clear error messages.
Frequently Asked Questions (FAQs)
Q1: How can I make this app production-ready?
A1: To go live, you need to: a) Use a process manager like PM2 to keep your Node.js app running. b) Use a cloud database like MongoDB Atlas. c) Use a cloud file storage service like AWS S3 or Google Cloud Storage instead of the local filesystem. d) Add proper logging and error monitoring.
Q2: Can I add user authentication to this?
A2: Absolutely! You can integrate Passport.js to allow users to sign up and log in. This would let you track all files uploaded by a specific user and create a "My Uploads" page.
Q3: Is it possible to allow multiple files in a single upload?
A3: Yes! Change upload.single('myfile')
to upload.array('myfiles', 10)
(the number 10 is the max number of files) and adjust the front-end form to have multiple
attribute on the file input.
Q4: My uploads are failing for large files. What should I do?
A4: Check the fileSize
limit in the Multer configuration. Also, you might need to adjust the body parser limit in Express and potentially the timeout settings on your reverse proxy (like Nginx) if you're using one.
Q5: How do I prevent malware from being uploaded?
A5: This is a critical security concern. While no method is 100% foolproof, you can: a) Restrict uploads to specific, safe file extensions (e.g., .pdf
, .jpg
, .mp4
). b) Use a virus scanning API (like VirusTotal) to scan the uploaded file before making the link active.
Conclusion
Congratulations! You've just built a fully functional, secure file sharing application with Node.js. You've worked with Express servers, handled file uploads with Multer, managed data with MongoDB and Mongoose, and created a dynamic front-end with EJS.
This project is more than just a file sharer; it's a template for understanding how modern web applications are structured. The patterns you've learned here—handling forms, managing state with a database, creating RESTful routes—are the building blocks of nearly every web app you'll ever build.
The journey from here involves adding more features, improving the UI with a framework like React, and deploying it for the world to use. If you enjoyed this deep dive and want to master these concepts systematically, to learn professional software development courses such as Python Programming, Full Stack Development, and MERN Stack, visit and enroll today at codercrafter.in. We provide structured, project-based learning to turn your curiosity into a career.