A lightweight note-taking web app built with Node.js, Express, and EJS โ no database required. Notes are stored as plain
.txtfiles on the server.
- ๐ Create notes from a clean web form
- ๐พ Store notes as
.txtfiles locally - ๐ View all notes on the home page
- ๐ Open and read individual notes
- โ๏ธ Edit existing notes (rename + update content)
- ๐จ Clean dark UI with Tailwind CSS
| Technology | Purpose |
|---|---|
| Node.js | Runtime environment |
| Express | Web framework |
| EJS | Template engine |
| fs module | File system storage |
| Tailwind CSS | Styling (via CDN) |
- Node.js 18+ โ Download here
- npm (comes with Node.js)
git clone git@github.com:mackcodes/notes-server-filesystem.gitcd notes-server-filesystemnpm install# Production
npm run start
# Development (auto-restart on file changes)
npm run devhttp://localhost:3000
notes-server-filesystem/
โโโ server.js โ main server file
โโโ package.json
โโโ README.md
โโโ files/
โ โโโ *.txt โ notes stored here
โโโ public/
โ โโโ images/
โ โ โโโ favicon.svg
โ โโโ javascripts/
โ โโโ stylesheets/
โโโ views/
โโโ index.ejs โ home page
โโโ edit.ejs โ edit note form
โโโ show.ejs โ single note page
| Method | Route | Description |
|---|---|---|
GET |
/ |
Render all notes |
POST |
/create |
Create a new note |
GET |
/file/:filename |
Render a single note |
GET |
/edit/:filename |
Render edit form for a note |
POST |
/edit |
Save note edits (rename + content update) |
- Click Edit on a note card from the home page.
- The app opens
/edit/:filenameand pre-fills:- current filename (readonly)
- editable new filename
- existing note details
- On submit, the app:
- renames the file if the name changed
- rewrites file content with updated title + details
- The title is stored on line 1 and details are stored from line 2 onward.
Example update flow:
Old file: LearnNode.txt
New file: LearnExpress.txt
Final file content:
LearnExpress
Updated note details...
Each note is saved as a .txt file like this:
My Note Title
This is the first line of details.
This is the second line of details.
- Line 1 โ used as the note title
- Remaining lines โ used as note content
EJS stands for Embedded JavaScript. It is a template engine that lets you write HTML with JavaScript logic inside it. Instead of sending plain HTML, your Express server processes the .ejs file, injects dynamic data into it, and then sends the final HTML to the browser.
Think of it like this:
Express (data) + EJS (template) = Final HTML sent to browser
Browser requests /
โ
Express reads all .txt files from /files
โ
Passes file list to index.ejs via res.render()
โ
EJS loops through files and builds HTML cards
โ
Final HTML is sent back to browser
In server.js:
app.get('/', function(req, res) {
fs.readdir('./files', function(err, files) {
res.render('index', { files: files, titles: titles });
// โ this data is sent to index.ejs
});
});In index.ejs, you receive and use that data:
<% files.forEach(function(val, index) { %>
<h1><%= titles[index] %></h1>
<% }) %>| Tag | Name | Purpose | Example |
|---|---|---|---|
<%= %> |
Output tag | Prints value to HTML (safe, escaped) | <%= username %> |
<% %> |
Scriptlet tag | Runs JS logic, prints nothing | <% if(x > 0) { %> |
<%- %> |
Unescaped tag | Prints raw HTML (be careful!) | <%- htmlContent %> |
<%# %> |
Comment tag | EJS comment, not sent to browser | <%# this is a note %> |
<%_ %> |
Whitespace tag | Removes whitespace before tag | <%_ if(x) { %> |
Used to display a variable value in HTML. EJS automatically escapes special characters to prevent XSS attacks.
<h1><%= title %></h1>
<p><%= filedata %></p>
<a href="/file/<%= filename %>">Open</a>Used to write JavaScript logic โ if, for, forEach, etc. Does not print anything to HTML on its own.
<% if (files.length > 0) { %>
<p>You have notes!</p>
<% } else { %>
<p>No notes yet.</p>
<% } %><% files.forEach(function(file, index) { %>
<div>
<h2><%= titles[index] %></h2>
</div>
<% }) %>Prints raw HTML directly. Use carefully โ never use with user input as it can cause security issues.
<%- '<strong>bold text</strong>' %>index.ejs โ loop through notes and render cards:
<% if(files.length > 0){ %>
<% files.forEach(function(val, index){ %>
<div class="task bg-zinc-800 rounded-md p-4">
<h1><%= titles[index] %></h1>
<a href="/file/<%= val %>">Read more</a>
</div>
<% }) %>
<% } else { %>
<h3>No tasks yet</h3>
<% } %>show.ejs โ display a single note:
<h1><%= filename %></h1>
<pre><%= filedata %></pre>app.set("view engine", "ejs"); // tell Express to use EJSres.render("index", { files: files, titles: titles });
// โ file name inside views/ โ data passed to templateEJS automatically looks inside the views/ folder โ no need to write the full path or .ejs extension.
Static files are served from the public/ folder.
app.use(express.static(path.join(__dirname, "public")));| File path | Browser path |
|---|---|
public/images/favicon.svg |
/images/favicon.svg |
Add favicon in your EJS files:
<link rel="icon" type="image/svg+xml" href="/images/favicon.svg" />- Confirm file exists at
public/images/favicon.svg - Use
/images/favicon.svg(not../public/...) - Hard refresh:
Ctrl + Shift + R
# Find and kill process on port 3000
kill -9 $(lsof -t -i:3000)- Make sure files end with
.txt - Make sure files are inside the
files/folder
- Add note delete feature
- Add validation for empty title/details
- Sanitize title into safe filenames
- Add timestamps (created/updated)
- Migrate to database storage (MongoDB/PostgreSQL)
- Add tests for routes and file operations
MIT ยฉ 2026