Build a Netflix-Style File Browser

Build a full-stack file browser with Express, a REST API, and a dark UI.

Introduction

30 Second Summary

Everyone has a folder somewhere on their computer stuffed with photos, videos, music, and documents. Finding anything in that mess usually means scrolling through an endless list of cryptic filenames.

In this project, you will build MediaVault, a local file browser that turns any folder on your computer into a Netflix-style browsing experience. You will create a full-stack web application with a dark-themed interface, horizontal scrolling rows, hover-to-expand file cards, and in-browser file previews.

What You'll Build

Imagine opening your browser and seeing every photo, video, song, and document from a folder on your computer organized into sleek horizontal rows you can scroll through, hover to expand, and click to preview right there.

By the end of this project, you'll have:

  • A running Express API that scans any folder on your machine and serves your real files grouped by category as JSON.
  • A dark, Netflix-style browsing interface with horizontal scroll rows, hover-to-expand card effects, and smooth scroll arrow navigation.
  • A working preview modal where clicking any file card opens it right in the browser. Images display, videos and audio play, and documents render in place.
  • Secret Mission: A live search bar that filters files across all categories as you type.

Are there any prerequisites?

No prior web development experience is needed. This guide walks you through every step from scratch.

Before We Start

Before we dive in, let's lock in what you're about to build and why it matters.

Set Up the Project

MediaVault is going to scan files, organize them into categories, and serve a web interface. All of that needs a backend that can listen for requests from the browser and send back responses.

In this step, you'll create the project folder, install Express (a Node.js framework for building web servers), and get a basic server running on your machine.

In this step, get ready to:
  • Create the project folder and install Express.
  • Set up the media and public folders with sample files.
  • Write and run a basic Express server.
Create the project folder and initialize it
  • Press the Super key (the key with the Windows logo) to open the Activities search.
  • Type Visual Studio Code and press Enter to open it.
  • Open the terminal inside VS Code by pressing Ctrl+`.
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create the project folder and move into it by running these commands:
mkdir mediavault
cd mediavault
  • Generate a package.json file by running:
npm init -y

What is package.json?

The package.json file is your project's manifest. It tracks your project's name, version, and most importantly, the list of packages (dependencies) your project needs to run.

  • Install Express by running:
npm install express

The terminal displays a summary confirming that Express and its dependencies were added to your project.

What is Express?

Express is a lightweight framework that makes it easy to build web servers with Node.js. It handles routing (deciding what happens when the browser visits a specific URL) and serving responses.

The npm install express command downloads the latest version of Express and saves it as a dependency in your package.json.

Seeing an error from npm install?

Make sure you're inside the mediavault folder. Run pwd to check your current directory.

If you see a network error, check your internet connection and try running the command again.

Help me fix my npm install error

  • Now open the mediavault folder in VS Code so the file explorer shows your project.
  • Click File in the top menu bar.
  • Click Open Folder.
  • Navigate to Desktop, select the mediavault folder, and click Open.

Seeing a trust prompt?

VS Code may ask "Do you trust the authors of the files in this folder?" Click Yes, I trust the authors to continue. This is normal for any new folder you open.

  • Open a new terminal by pressing Ctrl+`.
  • Confirm you're in the right folder by running:
ls

You should see node_modules, package.json, and package-lock.json listed in the output.

Don't see those files?

Your terminal may have opened in a different directory. Run cd ~/Desktop/mediavault to get back to the project folder, then run ls again.

Help me find my mediavault project folder

Create the folder structure

Your project needs two folders. The media folder holds the files you want to browse. The public folder will hold the frontend files (HTML, CSS, JavaScript) later.

  • Create both folders and a sample text file by running these commands:
mkdir media public
echo "This is a sample text file for MediaVault." > media/notes.txt
  • Verify the folders were created by running:
ls

You should see media and public alongside node_modules, package.json, and package-lock.json.

  • Copy a few more files from your computer into the media folder. Try to include at least one image and one other file type (a PDF, audio file, or video).
  • Use cp to copy files from anywhere on your system. For example:
cp ~/Pictures/photo.jpg media/
cp ~/Documents/report.pdf media/

No files handy?

You can create additional text files right in the terminal. For example, run echo "Another test" > media/readme.md to add a markdown file.

For images, try downloading a free image from the web and saving it into the media folder.

  • Check what's in your media folder by running:
ls media/

You should see notes.txt plus any other files you copied in.

Create and run the Express server

With the project initialized, it's time to write the server. This file is the entry point for your entire application.

  • In VS Code's Explorer sidebar on the left, hover over the MEDIAVAULT header.
  • Click the New File icon (it looks like a page with a plus sign).
  • Type server.js and press Enter.
  • Paste the following code into server.js:
// Import the Express framework
const express = require('express');

// Create an Express app and set the port number
const app = express();
const PORT = 3000;

// Send a welcome message when someone visits the root URL
app.get('/', (req, res) => {
  res.send('MediaVault is running');
});

// Start the server and log the URL to the terminal
app.listen(PORT, () => {
  console.log(`MediaVault running at http://localhost:${PORT}`);
});

What does this code do?

  • require('express') loads the Express framework you installed with npm.
  • express() creates your application instance. Every route and setting hangs off this object.
  • app.get('/', ...) tells the server what to do when a browser visits the root URL. It sends back the text "MediaVault is running".
  • app.listen(PORT, ...) starts the server on port 3000 and prints the URL so you know it's ready.
  • Save server.js by pressing Ctrl+S.

Before you run this, what do you think the browser will display when you visit http://localhost:3000?

  • Start the server by running this command in your terminal:
node server.js

You should see this message in the terminal:

MediaVault running at http://localhost:3000
  • Open your browser and visit http://localhost:3000.

The browser displays the text "MediaVault is running". Your server is live and responding to requests.

Don't see the message in your browser?

Check your terminal for error messages. A common cause is a typo in server.js. Make sure the file is saved and the code matches the example above exactly.

If you see a message about port 3000 already being in use, another process is occupying that port. Stop it by pressing Ctrl+C in the terminal, then run node server.js again.

Help me get my Express server running

✔️ Awesome, I've got everything!

Great. Double check you've saved server.js and keep the server running in your terminal.

ⓧ I'd like to double check the full code

Here is the complete server.js file. Compare it against yours line by line.

const express = require('express');

const app = express();
const PORT = 3000;

app.get('/', (req, res) => {
  res.send('MediaVault is running');
});

app.listen(PORT, () => {
  console.log(`MediaVault running at http://localhost:${PORT}`);
});

Your server is live and your project structure is in place. Next up, you'll build the API that scans your media folder and organizes your files into categories.

Build the File Scanner API

In the last step, you got your Express server running and confirmed it responds at http://localhost:3000.

The server can greet you in the browser, but it has no idea what files are sitting in your media/ folder. This step builds the backend engine that scans that folder, groups files by type, and serves the results as a JSON API your frontend can consume.

In this step, get ready to:
  • Import Node.js filesystem modules and define the file categories your scanner recognizes.
  • Write a function that reads a directory and groups files by category.
  • Create an API route that returns the categorized file data as JSON.
Import the fs and path modules and define file categories

Node.js ships with built-in modules for working with the filesystem. The fs module reads files and directories. The path module builds file paths that work on any operating system.

  • In server.js, find the line const express = require('express'); at the top of the file.
  • Add these two import lines directly below it:
const fs = require('node:fs');
const path = require('node:path');
  • Now find the line const PORT = 3000;. Add the following code below it to define where your media files live and which extensions belong to each category:
// Point to the folder that holds your media files
const MEDIA_DIR = path.join(__dirname, 'media');

// Map each file category to its recognized extensions
const FILE_CATEGORIES = {
  images: ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', '.bmp'],
  videos: ['.mp4', '.webm', '.mov', '.avi', '.mkv'],
  audio: ['.mp3', '.wav', '.ogg', '.flac', '.aac'],
  documents: ['.pdf', '.txt', '.md', '.doc', '.docx', '.xls', '.xlsx', '.csv']
};

What does this code do?

MEDIA_DIR uses path.join and __dirname to build a reliable absolute path to your media/ folder. This ensures the path resolves correctly no matter which directory you run node from.

FILE_CATEGORIES is a lookup table that maps category names to arrays of file extensions. When the scanner encounters a file, it checks this object to decide which category the file belongs to.

  • Save server.js.
  • Stop the running server by pressing Ctrl+C in the terminal, then restart it by running:
node server.js

You should see the same MediaVault running at http://localhost:3000 message as before. The server starts without errors, which confirms the new imports and constants are valid.

Seeing a syntax error on startup?

  • Check that each require line ends with a semicolon and uses 'node:fs' and 'node:path' (with the node: prefix).
  • Make sure the FILE_CATEGORIES object ends with }; and each array inside it is separated by a comma.
  • Help me debug this syntax error
Write the categorizeFiles function

With the categories defined, you need a function that reads every file in a directory, checks its extension, and drops it into the correct category bucket.

  • In server.js, add the following function below the FILE_CATEGORIES object. This is the function shell that reads the directory:
function categorizeFiles(dirPath) {
  // Start with empty arrays for each category
  const result = {
    images: [],
    videos: [],
    audio: [],
    documents: []
  };

  // Try reading all entries from the target directory
  let entries;
  try {
    entries = fs.readdirSync(dirPath, { withFileTypes: true });
  } catch (err) {
    return result;
  }

  return result;
}

What does this code do?

  • result starts as an object with four empty arrays, one per category. Files get pushed into these arrays as the scanner finds them.
  • fs.readdirSync with { withFileTypes: true } returns an array of Dirent objects. Each one has a .isFile() method so you can skip subdirectories without an extra stat call.
  • The try/catch block returns empty results gracefully if the folder does not exist, instead of crashing the server.

The function shell reads the directory, but it does not sort any files yet. Now you will add the loop that matches each file to a category.

  • Inside the categorizeFiles function, find the line that says return result;. Add this for loop directly above it:
  for (const entry of entries) {
    // Skip anything that is not a file
    if (!entry.isFile()) continue;

    // Get the file extension in lowercase
    const ext = path.extname(entry.name).toLowerCase();

    // Check which category this extension belongs to
    for (const [category, extensions] of Object.entries(FILE_CATEGORIES)) {
      if (extensions.includes(ext)) {
        const filePath = path.join(dirPath, entry.name);
        const stats = fs.statSync(filePath);
        result[category].push({
          name: entry.name,
          category: category,
          size: stats.size,
          modified: stats.mtime
        });
        break;
      }
    }
  }

What does this loop do?

  • The outer for loop walks through every entry in the directory. entry.isFile() skips subdirectories so only actual files are processed.
  • path.extname extracts the extension (including the dot), and .toLowerCase() normalizes it so .JPG and .jpg both match.
  • The inner loop checks each category's extension list. When it finds a match, it grabs the file's size and modification date with fs.statSync, pushes the file metadata into the right category array, and breaks out so a file is never counted twice.
  • Save server.js, stop the server with Ctrl+C, and restart it by running:
node server.js

The server starts with the same success message, confirming your function compiles without errors.

Server crashes with a ReferenceError?

  • Make sure the function is placed below FILE_CATEGORIES and above the existing app.get('/', ...) route.
  • Check for mismatched curly braces. The function should have a matching } at the end.
  • Help me fix this ReferenceError
Create the /api/files route

The scanner function is complete, but nothing in your server calls it. You need a REST API route that invokes the function and returns the results as JSON.

  • In server.js, find the existing route that handles /:
app.get('/', (req, res) => {
  res.send('MediaVault is running');
});
  • Replace that entire route with this new /api/files route:
app.get('/api/files', (req, res) => {
  // Scan the media folder and return categorized files
  const files = categorizeFiles(MEDIA_DIR);
  res.json(files);
});

What does this route do?

When a browser visits /api/files, Express calls the categorizeFiles function with the path to your media/ folder. The function scans the directory and returns a structured object. Then res.json() sends that object to the browser as a JSON response.

The old / route is gone now. Visiting the root URL will return a default response instead of the old greeting. The next step adds a proper HTML frontend there.

Before you restart the server, think about what you expect to see when you visit http://localhost:3000/api/files in the browser. What shape should the JSON have, given the sample files in your media/ folder?

  • Save server.js, stop the server with Ctrl+C, and restart it by running:
node server.js
  • Open http://localhost:3000/api/files in your browser.

You should see a JSON object with four keys: images, videos, audio, and documents. Each key holds an array of file objects from your media/ folder, with name, category, size, and modified fields. Categories with no matching files show empty arrays.

Seeing an empty JSON object or an error?

  • If all four arrays are empty, check that your media/ folder actually contains files with recognized extensions (like .jpg, .png, .txt, or .pdf).
  • If the browser shows a "Cannot GET" message for /api/files, make sure you saved server.js and restarted the server after your edits.
  • If you see a crash in the terminal mentioning that categorizeFiles is not defined, make sure the function is placed above the route in the file.
  • Help me debug my API response

✔️ Awesome, I've got everything!

Great. Make sure you have saved server.js before moving on.

ⓧ I'd like to double check the full code

Your server.js should now contain the following pieces, in this order. Scroll up and confirm each one is present:

  • Three require lines at the top: express, node:fs, and node:path.
  • app and PORT declarations.
  • MEDIA_DIR constant using path.join(__dirname, 'media').
  • FILE_CATEGORIES object with four keys (images, videos, audio, documents), each holding an array of extensions.
  • The categorizeFiles(dirPath) function with a result object, a try/catch around fs.readdirSync, a nested for loop that matches extensions and pushes file metadata, and a final return result.
  • A GET /api/files route (not the old GET / route) that calls categorizeFiles(MEDIA_DIR) and returns res.json(files).
  • The app.listen call at the bottom.

Your server now scans a real folder and returns structured file data as a JSON API. In the next step, you will build the dark Netflix-style frontend that turns this raw data into something you actually want to look at.

Build the Dark Netflix UI Shell

Your API returns structured data about every file in your media folder. But visiting your server right now just shows raw JSON in the browser. Nobody wants to browse their files by reading a JSON blob.

This step builds the cinematic dark interface that will eventually display your files in Netflix-style rows. You'll create the HTML skeleton, apply a dark theme with CSS, and wire your server to serve these frontend files.

In this step, get ready to:
  • Create the HTML page with a hero banner and gallery container.
  • Style the page with a dark theme, card layouts, and scroll effects.
  • Configure the server to serve your frontend files.
Create the HTML page

Every web interface starts with an HTML file that defines the page structure. This page needs a hero banner at the top and an empty gallery area where file cards will eventually appear.

  • Create a new file called index.html inside your public/ folder, and paste the following code:
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>MediaVault</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <header class="hero">
    <div class="hero-content">
      <h1>MediaVault</h1>
      <p>Your files. Organized. Beautiful.</p>
    </div>
  </header>

  <main id="gallery"></main>

  <script src="app.js" defer></script>
</body>
</html>

What does this HTML do?

  • The <link> tag connects a stylesheet called style.css that you'll create next.
  • The <header class="hero"> section creates the large banner area at the top of the page with the app title.
  • The <main id="gallery"> element is an empty container. JavaScript will fill it with file cards later.
  • The <script> tag with defer loads app.js after the HTML is fully parsed.
  • Save index.html.
  • Confirm the file was created by running:
ls public/

You should see index.html listed in the output.

Add the base styles and serve the frontend

The HTML page exists, but it has no styling and the server does not know to serve it. You'll add the foundational dark theme first, then update the server so you can see the result in your browser.

  • Create a new file called style.css inside your public/ folder, and paste the following code:
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  background-color: #141414;
  color: #ffffff;
  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
  overflow-x: hidden;
}

.hero {
  height: 300px;
  background: linear-gradient(to bottom, #1a1a2e, #141414);
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
}

.hero-content h1 {
  font-size: 3rem;
  color: #e50914;
  margin-bottom: 0.5rem;
}

.hero-content p {
  font-size: 1.2rem;
  color: #b3b3b3;
}

What do these styles do?

  • The * reset removes default browser margins and padding so every element starts from a clean slate.
  • The body background is set to #141414 (near-black) with white text, matching the Netflix dark theme.
  • The hero section uses a gradient from #1a1a2e (dark blue) down to the body color, with CSS Flexbox centering the title.
  • The h1 color #e50914 is the signature Netflix red.
  • Save style.css.

Your server is not serving these files to the browser yet. Right now it only responds to the /api/files route. You need to tell Express to serve everything in the public/ folder as static files.

  • In server.js, find this line near the bottom of the file:
app.get('/api/files', (req, res) => {
  • Add this line directly above it:
// Serve frontend files from the public folder
app.use(express.static(path.join(__dirname, 'public')));

What does express.static do?

When the browser requests /, Express looks inside the public/ folder for a file called index.html and serves it automatically. It does the same for style.css, app.js, and any other file in that folder.

Using path.join(__dirname, 'public') ensures the path resolves correctly regardless of where you launch node from.

  • Save server.js.

Before you restart the server, what do you think you'll see when you visit http://localhost:3000? Will it still show raw text, or something different?

  • Stop the running server by pressing Ctrl+C in the terminal.
  • Restart the server by running:
node server.js
  • Visit http://localhost:3000 in your browser.

You should see a dark page with a gradient background. The word MediaVault appears in bold red text, with the tagline Your files. Organized. Beautiful. below it in gray. The rest of the page is empty.

Seeing a white page or no styles?

  • Make sure index.html and style.css are both inside the public/ folder, not in the root mediavault/ folder.
  • Check that the <link> tag in your HTML says href="style.css" and not a path with public/ in it. Since Express serves from inside the public folder, the path is relative to that folder.
  • Confirm you restarted the server after saving server.js. Changes to server code require a restart.

Still stuck? Ask for help.

Complete the row and card styles

The dark hero banner is visible, but the page still needs styles for the file cards and horizontal scroll rows that will display your files. These styles target elements that your JavaScript will create in the next step.

  • In style.css, add the following code below the existing .hero-content p block:
/* Layout for each category section */
.category-row {
  padding: 1.5rem 3rem;
}

.category-row h2 {
  font-size: 1.4rem;
  margin-bottom: 0.75rem;
  color: #e5e5e5;
  text-transform: capitalize;
}

.row-container {
  position: relative;
}

/* Horizontal scrolling row of file cards */
.row-scroll {
  display: flex;
  gap: 10px;
  overflow-x: auto;
  scroll-behavior: smooth;
  padding: 1rem 0;
  scrollbar-width: none;
}

.row-scroll::-webkit-scrollbar {
  display: none;
}

What do these styles do?

  • Each .category-row is a section (like "Images" or "Videos") with an h2 heading.
  • The .row-scroll container uses Flexbox to lay cards out in a horizontal line. The overflow-x: auto allows scrolling when cards overflow, and scroll-behavior: smooth makes the scrolling animate.
  • The scrollbar-width: none and ::-webkit-scrollbar rules hide the scrollbar across all browsers for a clean Netflix look.
  • Save style.css and refresh your browser.

The dark hero page should still render correctly. The new styles are loaded but waiting for the file card elements you'll build in the next step.

  • Still in style.css, add the scroll button styles below the ::-webkit-scrollbar block:
/* Arrow buttons that appear on row hover */
.scroll-btn {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  background: rgba(0, 0, 0, 0.7);
  color: #ffffff;
  border: none;
  font-size: 2rem;
  padding: 1rem 0.5rem;
  cursor: pointer;
  z-index: 2;
  opacity: 0;
  transition: opacity 0.3s;
  height: 100%;
}

.row-container:hover .scroll-btn {
  opacity: 1;
}

.scroll-btn-left {
  left: 0;
}

.scroll-btn-right {
  right: 0;
}

What do these scroll button styles do?

The scroll buttons sit at the left and right edges of each row, positioned absolutely so they float over the cards. They start invisible (opacity: 0) and fade in when the user hovers over the row container. This keeps the interface clean until the learner needs to scroll.

  • Save style.css.
  • Next, add the file card styles below the .scroll-btn-right block:
/* Individual file cards */
.file-card {
  min-width: 200px;
  height: 130px;
  background-color: #2a2a2a;
  border-radius: 6px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: transform 0.3s, box-shadow 0.3s;
  padding: 1rem;
  flex-shrink: 0;
}

.file-card:hover {
  transform: scale(1.15);
  z-index: 1;
  box-shadow: 0 8px 25px rgba(0, 0, 0, 0.6);
}

What makes the cards pop on hover?

Each card uses flex-shrink: 0 so it keeps its 200px width inside the flex row instead of collapsing. On hover, transform: scale(1.15) enlarges the card by 15% using a GPU-accelerated transform. The z-index: 1 lifts it above neighboring cards so it overlaps cleanly.

  • Save style.css and refresh your browser to confirm the page still loads correctly.

The hero banner and dark background should still be rendering. The card styles are ready and waiting for the JavaScript that will create the card elements.

  • Finally, add the card text styles and empty state message below the .file-card:hover block:
/* Icon and filename inside each card */
.file-card .file-icon {
  font-size: 2.5rem;
  margin-bottom: 0.5rem;
}

.file-card .file-name {
  font-size: 0.8rem;
  color: #b3b3b3;
  text-align: center;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 180px;
}

/* Shown when no files match */
.empty-message {
  color: #666;
  font-style: italic;
  padding: 1rem 3rem;
}

What do these styles handle?

  • The .file-name uses text-overflow: ellipsis to truncate long filenames with "..." instead of breaking the card layout.
  • The .empty-message style formats a fallback message that appears when no files are found in a category.
  • Save style.css.

✔️ Awesome, I've got everything!

Great. Make sure you've saved style.css before continuing.

ⓧ I'd like to double check the full code

Your style.css file is longer than a single screen, so scroll through it and confirm you have these sections in order:

  • The * reset, body, and .hero styles (including .hero-content h1 and .hero-content p).
  • The .category-row, .row-container, .row-scroll, and scrollbar-hiding rules.
  • The .scroll-btn styles with the hover reveal, .scroll-btn-left, and .scroll-btn-right.
  • The .file-card base and hover styles.
  • The .file-icon, .file-name, and .empty-message styles at the bottom.

Styles not applying correctly?

  • Open your browser's developer tools (F12 or right-click and select Inspect). Check the Console tab for any CSS file loading errors.
  • Confirm every CSS block has matching opening { and closing } braces. A missing brace can break all rules that follow it.
  • Make sure every property line ends with a semicolon. A missing semicolon can cause the next property to be ignored.

Need more help? Ask here.

Refresh your browser one more time to confirm everything is stable. The dark page with the red MediaVault title and gradient background should render cleanly. The gallery area below the hero is empty because nothing connects the UI to the API yet.

✔️ Awesome, I've got everything!

Great. Confirm you've saved both server.js and style.css.

ⓧ I'd like to double check the full code

Your index.html should look exactly like this:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>MediaVault</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <header class="hero">
    <div class="hero-content">
      <h1>MediaVault</h1>
      <p>Your files. Organized. Beautiful.</p>
    </div>
  </header>

  <main id="gallery"></main>

  <script src="app.js" defer></script>
</body>
</html>

Your server.js should have this line added above the /api/files route:

app.use(express.static(path.join(__dirname, 'public')));

For style.css, scroll up and check each section matches the five code blocks you added.

Your cinematic dark interface is live. Next up, you'll connect this UI to your file scanner API so your real files appear as browsable cards inside these Netflix-style rows.

Connect the UI to Your Real Files

In the last step, you built a cinematic dark interface with a hero banner and all the CSS ready for category rows and cards. The page looks great, but the gallery below the banner is completely empty.

Your API at /api/files is already returning structured file data. In this step, you will write JavaScript that fetches that data and dynamically builds browsable file cards inside the Netflix-style rows.

In this step, get ready to:
  • Fetch your file data from the Express API using the browser's fetch API.
  • Render category rows with file cards that scale up on hover.
  • Add scroll arrow buttons to browse each row horizontally.
Create app.js and fetch the file data

Your index.html already has a script tag that loads app.js, but the file does not exist yet. The browser's fetch function lets you send HTTP requests from your frontend to your own server.

Before creating the file, make a small cleanup to the script tag in your HTML.

  • In public/index.html, find this line near the bottom of the file:
  <script src="app.js" defer></script>
  • Remove the defer attribute so the line looks like this:
  <script src="app.js"></script>
  • Save index.html.
  • Create a new file called app.js inside the public/ folder and paste the following code:
// Map each file category to an emoji icon
const CATEGORY_ICONS = {
  images: '🖼️',
  videos: '🎬',
  audio: '🎵',
  documents: '📄'
};

// Fetch file data from the API and render it
async function loadFiles() {
  const response = await fetch('/api/files');
  const files = await response.json();
  renderGallery(files);
}

loadFiles();

What does this code do?

CATEGORY_ICONS maps each file category to an emoji so every card gets a visual icon matching its type.

loadFiles() uses the browser's fetch function to request your file data from /api/files, parses the JSON response, and hands it off to a renderGallery function you have not written yet. The call at the bottom kicks everything off when the page loads.

  • Save app.js.
  • Refresh http://localhost:3000 in your browser.
  • Press F12 to open DevTools, then click the Console tab.

You should see a red error message saying renderGallery is not defined. This is expected. The fetch succeeded, but the function it calls does not exist yet.

Not seeing any error at all?

  • Check that your file is saved as public/app.js (inside the public folder, not in the project root).
  • Make sure the script tag in index.html reads <script src="app.js"></script> and both files are saved.
  • Make sure the server is still running. If it stopped, run node server.js again.

Still stuck? Ask for help.

Write the renderGallery function

The renderGallery function takes the JSON data from your API and turns it into DOM elements: one row per category, one card per file.

  • In app.js, add the first part of the renderGallery function below the loadFiles function and above the loadFiles() call at the bottom:
function renderGallery(files) {
  // Find the gallery container and clear previous content
  const gallery = document.getElementById('gallery');
  gallery.innerHTML = '';

  for (const [category, items] of Object.entries(files)) {
    // Skip categories with no files
    if (items.length === 0) continue;

    // Create a labeled section for this category
    const section = document.createElement('section');
    section.className = 'category-row';

    const heading = document.createElement('h2');
    heading.textContent = category;
    section.appendChild(heading);

    // Build the scrollable row container
    const container = document.createElement('div');
    container.className = 'row-container';

    const row = document.createElement('div');
    row.className = 'row-scroll';
  • Directly below the code you just added (still inside the renderGallery function), paste the card creation loop and the closing of the function:
    // Create a card for each file in this category
    for (const file of items) {
      const card = document.createElement('div');
      card.className = 'file-card';

      const icon = document.createElement('span');
      icon.className = 'file-icon';
      icon.textContent = CATEGORY_ICONS[category];
      card.appendChild(icon);

      const name = document.createElement('span');
      name.className = 'file-name';
      name.textContent = file.name;
      card.appendChild(name);

      row.appendChild(card);
    }

    // Assemble the row and add it to the gallery
    container.appendChild(row);
    section.appendChild(container);
    gallery.appendChild(section);
  }

  // Show a message if no files were found
  if (gallery.children.length === 0) {
    const msg = document.createElement('p');
    msg.className = 'empty-message';
    msg.textContent = 'No media files found. Add files to the media folder and refresh.';
    gallery.appendChild(msg);
  }
}

What does renderGallery do?

  • gallery.innerHTML = '' clears the gallery so calling the function again does not duplicate rows.
  • The outer loop walks each category from the API response. Empty categories are skipped with continue.
  • For each category, a section.category-row is created with an h2 heading and a scrollable row-scroll flex container.
  • The inner loop creates a file-card div for every file, with an emoji icon and the file name. The CSS you wrote in Step 3 styles these cards automatically.
  • If no categories have any files, a fallback message is shown instead.
  • Save app.js.
  • Refresh http://localhost:3000 in your browser.

You should see your real files from the media/ folder organized into labeled category rows. Hover over a card to watch it scale up with a shadow effect.

Still seeing an empty page?

  • Open the browser console (F12). If you see a SyntaxError, double check that all your curly braces and parentheses match. The renderGallery function has two nested for loops and an if block, so closing braces are easy to miss.
  • If the console shows no errors but the page is blank, make sure your media/ folder has files in it. Visit http://localhost:3000/api/files to confirm the API returns file data.
  • Make sure renderGallery is placed above the loadFiles() call at the bottom of the file. JavaScript needs to see the function before it is called.

Need a hand? Ask for help.

Wire up the scroll arrows

Your CSS already has styles for scroll arrow buttons that appear on hover. The buttons need to exist in the DOM and have click listeners that scroll the row.

  • In app.js, find this line inside renderGallery:
    const container = document.createElement('div');
    container.className = 'row-container';
  • Directly below those two lines, add the left scroll button:
    // Left scroll button
    const btnLeft = document.createElement('button');
    btnLeft.className = 'scroll-btn scroll-btn-left';
    btnLeft.textContent = '\u2039';
    container.appendChild(btnLeft);
  • Now find this line further down in the same function:
    container.appendChild(row);
  • Directly below that line, add the right scroll button and the click listeners for both arrows:
    // Right scroll button
    const btnRight = document.createElement('button');
    btnRight.className = 'scroll-btn scroll-btn-right';
    btnRight.textContent = '\u203A';
    container.appendChild(btnRight);

    // Scroll the row when arrow buttons are clicked
    btnLeft.addEventListener('click', () => {
      row.scrollBy({ left: -row.clientWidth * 0.8, behavior: 'smooth' });
    });

    btnRight.addEventListener('click', () => {
      row.scrollBy({ left: row.clientWidth * 0.8, behavior: 'smooth' });
    });

How do the scroll arrows work?

Each arrow button calls row.scrollBy() with 80% of the row's visible width. This pages through the cards in chunks, just like Netflix's row navigation.

The behavior: 'smooth' option animates the scroll instead of jumping. The CSS you wrote earlier keeps the buttons hidden until you hover over the row container.

Before you test, what do you think will happen when you hover over a category row?

  • Save app.js.
  • Refresh http://localhost:3000 in your browser.
  • Hover over any category row to reveal the scroll arrows.
  • Click an arrow to watch the row scroll smoothly through your file cards.

You should see translucent arrow buttons appear on the left and right edges of each row. Clicking them scrolls through the cards with a smooth animation. If a row has only a few cards, the arrows still appear but there is nothing extra to scroll to.

Scroll arrows not appearing?

  • Make sure the btnLeft code is placed after container.className = 'row-container' and before const row. The btnRight code should be after container.appendChild(row).
  • Check that the button class names match your CSS exactly: scroll-btn scroll-btn-left and scroll-btn scroll-btn-right.
  • The arrows only appear on hover. Move your mouse over the row area and look at the left and right edges.

Still not working? Ask for help.

✔️ Awesome, I've got everything!

Great. Make sure you have saved app.js.

ⓧ I'd like to double check the full code

Your app.js file should contain these sections in order. Scroll up through the code blocks above and confirm each one is present.

  • CATEGORY_ICONS object mapping four categories to emoji icons.
  • loadFiles async function that fetches /api/files and calls renderGallery.
  • renderGallery function with: gallery clear, outer category loop, section/heading creation, btnLeft creation, row creation, inner card loop, btnRight creation, scroll event listeners, assembly, and the empty-message fallback.
  • loadFiles() call at the very bottom.

Your files are organized, browsable, and responding to hover. Next up, you will make those cards actually do something by adding a preview modal that opens images, plays videos and audio, and renders documents right in the browser.

Add File Preview

Your gallery looks great and your files are organized into rows. But clicking a card does nothing. A file browser that can't open files is like a library with locked books.

In this step, you'll build a preview system that opens files right inside the browser. Images display, videos and audio play with controls, and text files render their contents. All of it happens inside a modal overlay that floats on top of the gallery.

In this step, get ready to:
  • Add a preview modal to the HTML and serve media files from the backend.
  • Style the modal with a dark overlay, centered content box, and responsive preview elements.
  • Wire up JavaScript to open file previews on card click and close them with the backdrop or close button.
Add the preview modal HTML and serve media files

The preview needs two things to work. First, the HTML page needs a hidden modal container that JavaScript can reveal when the learner clicks a card. Second, the server needs a route that serves the actual file bytes so the browser can display them.

  • In public/index.html, find the closing </main> tag.
  • Add the following modal markup between </main> and the <script> tag:
  <div id="preview-modal" class="modal hidden">
    <div class="modal-backdrop"></div>
    <div class="modal-content">
      <button class="modal-close">&times;</button>
      <div id="preview-container"></div>
      <p id="preview-filename"></p>
    </div>
  </div>

What does this markup do?

The outer #preview-modal div is the full-screen overlay. It starts with class hidden so it stays invisible until JavaScript removes that class.

Inside it, .modal-backdrop is the dark semi-transparent background the user clicks to close. .modal-content holds the preview itself, a close button, and the filename.

  • Save index.html.

✔️ Awesome, I've got everything!

Great. Double check you've saved your file.

ⓧ I'd like to double check the full code

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>MediaVault</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <header class="hero">
    <div class="hero-content">
      <h1>MediaVault</h1>
      <p>Your files. Organized. Beautiful.</p>
    </div>
  </header>

  <main id="gallery"></main>

  <div id="preview-modal" class="modal hidden">
    <div class="modal-backdrop"></div>
    <div class="modal-content">
      <button class="modal-close">&times;</button>
      <div id="preview-container"></div>
      <p id="preview-filename"></p>
    </div>
  </div>

  <script src="app.js"></script>
</body>
</html>

Now the browser has a place to show previews, but it still can't access the actual files. Your Express server currently serves only the public/ folder. You need a second static route that exposes the media/ folder so the browser can load images, videos, and documents.

  • In server.js, find this line:
app.use(express.static(path.join(__dirname, 'public')));
  • Add this new line directly below it:
app.use('/media', express.static(MEDIA_DIR));

What does this route do?

This tells Express to serve any file inside your media/ folder when the browser requests a URL starting with /media/. For example, a file called photo.jpg in your media folder becomes accessible at /media/photo.jpg.

  • Save server.js.
  • Restart the server by pressing Ctrl+C in the terminal, then running:
node server.js
  • Open your browser and navigate to http://localhost:3000/media/ followed by the name of one of the files in your media/ folder.

You should see the file load directly in the browser. An image displays, a text file shows its content, and audio or video files start a player.

Seeing a "Cannot GET" error?

  • Double check the filename in the URL matches exactly what's in your media/ folder. Filenames are case-sensitive.
  • Make sure the new app.use('/media', ...) line sits below the existing express.static line for public/, not inside another route.
  • Confirm you restarted the server after saving.

Help me debug why my /media route isn't serving files.

✔️ Awesome, I've got everything!

Great. Double check you've saved your file.

ⓧ I'd like to double check the full code

Your server.js is over 30 lines, so here is a focused check. Scroll up and confirm this line appears directly below app.use(express.static(path.join(__dirname, 'public')));:

app.use('/media', express.static(MEDIA_DIR));
Add modal styles

If you refresh your browser right now, you'll notice some raw text from the modal div sitting at the bottom of the page. The modal needs CSS to position it as a fullscreen overlay and hide it until a card is clicked.

  • In public/style.css, add the modal overlay and backdrop styles at the bottom of the file:
/* Fullscreen modal overlay */
.modal {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: center;
}

.modal.hidden {
  display: none;
}

/* Dark semi-transparent backdrop */
.modal-backdrop {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: rgba(0, 0, 0, 0.85);
}

What do these styles do?

The .modal class pins the overlay to the full viewport with position: fixed and centers its child content using Flexbox.

The .modal.hidden rule hides the whole thing with display: none. JavaScript will toggle this class to show and hide the modal.

  • Save style.css and refresh your browser.

The raw modal text that appeared at the bottom of the page is now gone. The hidden class is doing its job.

  • Add the content box and close button styles below the backdrop rules:
/* Centered content box */
.modal-content {
  position: relative;
  background: #1e1e1e;
  border-radius: 10px;
  padding: 2rem;
  max-width: 80vw;
  max-height: 80vh;
  display: flex;
  flex-direction: column;
  align-items: center;
}

/* Close button in the top-right corner */
.modal-close {
  position: absolute;
  top: 0.5rem;
  right: 1rem;
  background: none;
  border: none;
  color: #ffffff;
  font-size: 2rem;
  cursor: pointer;
}

What do these styles do?

The .modal-content box floats on top of the backdrop with a dark background, rounded corners, and size limits so previews never overflow the screen.

The .modal-close button sits in the top-right corner using position: absolute. It has no background or border so it blends into the modal.

  • Add the preview element styles below the close button rules. These control how images, videos, audio, PDFs, and text files render inside the modal:
/* Preview elements: images, video, audio */
#preview-container img,
#preview-container video,
#preview-container audio {
  max-width: 70vw;
  max-height: 60vh;
  border-radius: 6px;
}

/* PDF preview via iframe */
#preview-container iframe {
  width: 70vw;
  height: 60vh;
  border: none;
  border-radius: 6px;
}

/* Text file preview */
#preview-container pre {
  max-width: 70vw;
  max-height: 60vh;
  overflow: auto;
  background: #2a2a2a;
  padding: 1rem;
  border-radius: 6px;
  color: #e5e5e5;
  font-size: 0.9rem;
  white-space: pre-wrap;
}

#preview-filename {
  margin-top: 1rem;
  color: #b3b3b3;
  font-size: 0.9rem;
}

What do these styles do?

Each file type gets its own sizing rules. Images and videos are capped at 70% of the viewport width and 60% of the height so they fit comfortably.

PDFs load inside an iframe with a fixed size. Text files use a pre element with a scrollable overflow so long files stay contained.

  • Save style.css.

✔️ Awesome, I've got everything!

Great. Double check you've saved your file. The styles activate once JavaScript opens the modal in the next substep.

ⓧ I'd like to double check the full code

Your style.css is now quite long. Scroll up and confirm these sections appear at the end of the file, in this order:

  • .modal (fixed fullscreen overlay).
  • .modal.hidden (display none).
  • .modal-backdrop (dark semi-transparent background).
  • .modal-content (centered box with max dimensions).
  • .modal-close (absolute top-right close button).
  • #preview-container img/video/audio, iframe, pre (responsive preview sizing).
  • #preview-filename (gray label below the preview).
Add preview logic

The HTML and CSS are ready. Now you need the JavaScript that actually opens files when a card is clicked. You'll write an openPreview function that picks the right DOM element for each file type, and a closePreview function that tears it down.

  • In public/app.js, add the openPreview function below the closing } of the renderGallery function. Start with support for images, videos, and audio:
function openPreview(file) {
  const modal = document.getElementById('preview-modal');
  const container = document.getElementById('preview-container');
  const filename = document.getElementById('preview-filename');

  container.innerHTML = '';
  filename.textContent = file.name;

  // Build the URL to the file served from /media
  const fileUrl = '/media/' + encodeURIComponent(file.name);

  // Create the right HTML element for each file type
  if (file.category === 'images') {
    const img = document.createElement('img');
    img.src = fileUrl;
    img.alt = file.name;
    container.appendChild(img);
  } else if (file.category === 'videos') {
    const video = document.createElement('video');
    video.src = fileUrl;
    video.controls = true;
    container.appendChild(video);
  } else if (file.category === 'audio') {
    const audio = document.createElement('audio');
    audio.src = fileUrl;
    audio.controls = true;
    container.appendChild(audio);
  }
  modal.classList.remove('hidden');
}

What does this function do?

When called with a file object, openPreview clears any previous preview, builds the URL to the file on the server (/media/filename), and creates the appropriate HTML element for the file's category.

Images get an img tag. Videos and audio get their own elements with controls = true so the browser renders play/pause buttons automatically. Finally, it removes the hidden class to reveal the modal.

  • Below openPreview, add the closePreview function and event listeners that wire up the backdrop and close button:
function closePreview() {
  const modal = document.getElementById('preview-modal');
  modal.classList.add('hidden');
  document.getElementById('preview-container').innerHTML = '';
}

// Close when clicking the dark backdrop or the X button
document.querySelector('.modal-backdrop').addEventListener('click', closePreview);
document.querySelector('.modal-close').addEventListener('click', closePreview);

What does this code do?

The closePreview function adds the hidden class back to the modal and clears the preview container so stale content doesn't linger.

The two event listeners let the user close the modal by clicking either the dark backdrop area or the X button.

Both functions are defined, but nothing calls openPreview yet. You need to connect each file card to it.

  • Scroll up to the renderGallery function and find these two lines inside the card creation loop:
      const card = document.createElement('div');
      card.className = 'file-card';
  • Add this click listener on the line directly below card.className = 'file-card'; so the block looks like this:
      const card = document.createElement('div');
      card.className = 'file-card';
      card.addEventListener('click', () => openPreview(file));
  • Save app.js.
  • Restart the server by pressing Ctrl+C and running:
node server.js

Before you click a card, what do you expect to see when you click an image file?

  • Visit http://localhost:3000 and click on an image card.

The image appears in a centered modal overlay on top of a dark backdrop. Click the X button or the dark area behind the modal to close it. Try clicking a video or audio card too. You should see playback controls.

Clicking a card does nothing?

  • Open your browser's developer console (F12, then the Console tab). Look for a red error message. A ReferenceError means openPreview is not defined. Check that you added the function below renderGallery.
  • Verify the card.addEventListener line is inside the for (const file of items) loop, right after card.className = 'file-card'.
  • Make sure you restarted the server and hard-refreshed the page (Ctrl+Shift+R) to clear the cached JavaScript.

Help me debug why clicking a card doesn't open the preview.

Images, videos, and audio work. But if you click a text file or PDF card, the modal opens with an empty preview. The function doesn't handle documents yet.

  • In the openPreview function, find the closing bracket of the audio branch:
    container.appendChild(audio);
  }
  modal.classList.remove('hidden');
  • Replace the } on the line between container.appendChild(audio) and modal.classList.remove('hidden') with this document handling branch:
  } else if (file.category === 'documents') {
    // Check if it's a PDF or a text-based file
    const ext = file.name.split('.').pop().toLowerCase();
    if (ext === 'pdf') {
      const iframe = document.createElement('iframe');
      iframe.src = fileUrl;
      container.appendChild(iframe);
    } else {
      // Fetch text content and display in a pre element
      fetch(fileUrl)
        .then(res => res.text())
        .then(text => {
          const pre = document.createElement('pre');
          pre.textContent = text;
          container.appendChild(pre);
        });
    }
  }

Why are documents handled differently?

Images, videos, and audio can point an HTML element's src at a URL and the browser renders them natively. Documents need a different approach.

PDFs load inside an iframe because the browser's built-in PDF viewer handles the rendering. Text files like .txt and .md are fetched as raw text with the fetch API and displayed in a pre element to preserve formatting.

  • Make sure the line after your new block still reads modal.classList.remove('hidden'); so the modal still opens after creating the preview element.
  • Save app.js.
  • Restart the server (Ctrl+C, then node server.js) and refresh the browser.
  • Click a text file card.

The modal opens and displays the file's text content in a scrollable code-style box. If you have a PDF in your media/ folder, try clicking that too. It loads inside the browser's built-in PDF viewer.

Text file preview shows a blank modal?

  • Open the browser console (F12). If you see a network error for the /media/ request, check that app.use('/media', express.static(MEDIA_DIR)) is in your server.js.
  • Make sure the } else if (file.category === 'documents') block replaced the closing } after the audio branch, not the function's closing brace.

Help me debug why my text file preview isn't showing content.

✔️ Awesome, I've got everything!

Great. Double check you've saved your file.

ⓧ I'd like to double check the full code

Your app.js is quite long now. Scroll through and confirm these sections appear in order:

  • CATEGORY_ICONS object at the top.
  • loadFiles function that fetches /api/files.
  • renderGallery function with card.addEventListener('click', () => openPreview(file)) inside the card creation loop.
  • openPreview function with branches for images, videos, audio, and documents.
  • closePreview function.
  • Event listeners on .modal-backdrop and .modal-close.
  • loadFiles() call at the very bottom.

Time for the full test. Restart the server one more time and refresh the browser.

  • Click an image card. The image displays in a centered modal overlay.
  • Click a video or audio card. You see playback controls.
  • Click a text file card. The file's contents render in a scrollable box.
  • Click the dark backdrop or the X button. The modal closes.

Your file browser is fully functional. You can scan files, browse them in a Netflix-style gallery, and preview every file type right in the browser. Next up, the Secret Mission challenges you to add a live search bar that filters files across all categories as you type.

Secret mission

Add Live Search

With dozens of files across multiple categories, scrolling through every row to find one specific file gets tedious. Can you add a search bar that filters files across all categories in real time as you type?

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely on your local machine and uses no paid services or cloud resources. There are no ongoing costs.

Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • The mediavault/ project folder on your Desktop (contains server.js, public/, media/, and node_modules/).
  • The running Node.js server process (if still active in your terminal).

Keep everything running

No action needed. Choose this if you want to keep using MediaVault to browse your files.

  • Leave the server running with node server.js whenever you want to browse your files at http://localhost:3000.
  • Try pointing it at different folders by changing the MEDIA_DIR path in server.js to browse any folder on your computer.

Pause - I'll come back to this later

Stop the server to free up your terminal. Your project files stay on disk, ready to restart anytime.

  • Press Ctrl+C in the terminal where your server is running to stop it.
  • When you want to come back, open a terminal in your mediavault/ folder and run node server.js again.

Delete - I don't want to use this again

Remove all project files from your machine. This deletes your code and your media folder contents.

  • Press Ctrl+C in the terminal to stop the server if it is still running.
  • Delete the entire project folder by running:
rm -rf ~/Desktop/mediavault
  • Confirm the folder is gone by running:
ls ~/Desktop

You should no longer see mediavault in the output.

Nice Work!

Nice Work!

Nice work. You just built MediaVault, a full-stack file browser that turns a messy folder into a polished, Netflix-style browsing experience.

You've learned how to:

  • Build a full-stack Express server that reads your local filesystem with Node.js fs and path modules and serves categorized file metadata through a REST API endpoint.
  • Create a dark Netflix-style frontend with CSS Flexbox horizontal scroll rows, hover-to-expand card effects, and dynamically rendered file cards powered by the fetch API.
  • Build an in-browser preview modal that displays images, plays video and audio, and renders text and PDF documents without leaving the page.
  • Secret Mission: Add a live search bar that filters files across all categories in real time as you type.

Ready to quiz yourself?