Project 3 - A Notetaking Web App
Project Overview
Build a complete full-stack notetaking application with:
- Frontend: React or vanilla JavaScript
- Backend: Node.js with Express
- Database: JSON file or simple in-memory
- Features: Create, read, update, delete notes
- Authentication: Simple user login
Functional Requirements
- User registration and login
- Create, view, edit, delete notes
- Notes persist (save to database)
- Search/filter notes
- Responsive design
- User-specific notes
Technical Requirements
- Express.js backend
- RESTful API
- JSON file or MongoDB
- Frontend form handling
- Authentication tokens
- Error handling
Project Structure
notetaking-app/
├── backend/
│ ├── server.js
│ ├── routes/
│ │ ├── auth.js
│ │ └── notes.js
│ ├── data/
│ │ ├── users.json
│ │ └── notes.json
│ └── package.json
├── frontend/
│ ├── index.html
│ ├── style.css
│ ├── script.js
│ └── app.js
└── README.md
Backend Implementation
server.js
const express = require("express");
const path = require("path");
const app = express();
// Middleware
app.use(express.json());
app.use(express.static("../frontend"));
// Routes
const authRoutes = require("./routes/auth");
const notesRoutes = require("./routes/notes");
app.use("/api/auth", authRoutes);
app.use("/api/notes", notesRoutes);
// Error handling
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({ error: "Internal server error" });
});
const PORT = 3000;
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
routes/notes.js
const express = require("express");
const router = express.Router();
const fs = require("fs").promises;
const path = require("path");
const DATA_FILE = path.join(__dirname, "../data/notes.json");
// Get all notes for user
router.get("/", async (req, res) => {
try {
let data = await fs.readFile(DATA_FILE, "utf8");
let notes = JSON.parse(data);
// Filter by user
let userNotes = notes.filter(n => n.userId === req.userId);
res.json(userNotes);
} catch (error) {
res.json([]);
}
});
// Create note
router.post("/", async (req, res) => {
try {
let notes = [];
try {
let data = await fs.readFile(DATA_FILE, "utf8");
notes = JSON.parse(data);
} catch (e) { }
let newNote = {
id: Date.now(),
userId: req.userId,
title: req.body.title,
content: req.body.content,
createdAt: new Date()
};
notes.push(newNote);
await fs.writeFile(DATA_FILE, JSON.stringify(notes, null, 2));
res.status(201).json(newNote);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Update note
router.put("/:id", async (req, res) => {
try {
let data = await fs.readFile(DATA_FILE, "utf8");
let notes = JSON.parse(data);
let note = notes.find(n => n.id == req.params.id && n.userId === req.userId);
if (!note) return res.status(404).json({ error: "Note not found" });
note.title = req.body.title;
note.content = req.body.content;
note.updatedAt = new Date();
await fs.writeFile(DATA_FILE, JSON.stringify(notes, null, 2));
res.json(note);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Delete note
router.delete("/:id", async (req, res) => {
try {
let data = await fs.readFile(DATA_FILE, "utf8");
let notes = JSON.parse(data);
let index = notes.findIndex(n => n.id == req.params.id && n.userId === req.userId);
if (index === -1) return res.status(404).json({ error: "Note not found" });
let deleted = notes.splice(index, 1);
await fs.writeFile(DATA_FILE, JSON.stringify(notes, null, 2));
res.json(deleted[0]);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
module.exports = router;
Frontend Implementation
index.html
<!DOCTYPE html>
<html>
<head>
<title>Notes App</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="app">
<div id="loginForm" class="form-container">
<h1>Login</h1>
<input type="email" id="loginEmail" placeholder="Email">
<input type="password" id="loginPassword" placeholder="Password">
<button onclick="login()">Login</button>
</div>
<div id="notesApp" class="hidden">
<header>
<h1>My Notes</h1>
<button onclick="logout()">Logout</button>
</header>
<div class="form-container">
<input type="text" id="noteTitle" placeholder="Title">
<textarea id="noteContent" placeholder="Content"></textarea>
<button onclick="saveNote()">Save Note</button>
</div>
<div id="notesList" class="notes-list"></div>
</div>
</div>
<script src="app.js"></script>
</body>
</html>
app.js
let currentUser = null;
async function login() {
let email = document.getElementById("loginEmail").value;
let password = document.getElementById("loginPassword").value;
try {
let response = await fetch("/api/auth/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, password })
});
let result = await response.json();
if (result.token) {
currentUser = { email, token: result.token };
localStorage.setItem("token", result.token);
showNotesApp();
loadNotes();
} else {
alert("Login failed");
}
} catch (error) {
console.error("Error:", error);
}
}
async function saveNote() {
let title = document.getElementById("noteTitle").value;
let content = document.getElementById("noteContent").value;
if (!title || !content) {
alert("Please fill in all fields");
return;
}
try {
let response = await fetch("/api/notes", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer " + currentUser.token
},
body: JSON.stringify({ title, content })
});
if (response.ok) {
document.getElementById("noteTitle").value = "";
document.getElementById("noteContent").value = "";
loadNotes();
}
} catch (error) {
console.error("Error:", error);
}
}
async function loadNotes() {
try {
let response = await fetch("/api/notes", {
headers: {
"Authorization": "Bearer " + currentUser.token
}
});
let notes = await response.json();
let list = document.getElementById("notesList");
list.innerHTML = "";
notes.forEach(note => {
list.innerHTML += `
<div class="note">
<h3>${note.title}</h3>
<p>${note.content}</p>
<button onclick="deleteNote(${note.id})">Delete</button>
</div>
`;
});
} catch (error) {
console.error("Error:", error);
}
}
async function deleteNote(id) {
if (!confirm("Delete this note?")) return;
try {
await fetch(`/api/notes/${id}`, {
method: "DELETE",
headers: {
"Authorization": "Bearer " + currentUser.token
}
});
loadNotes();
} catch (error) {
console.error("Error:", error);
}
}
function logout() {
currentUser = null;
localStorage.removeItem("token");
showLoginForm();
}
function showLoginForm() {
document.getElementById("loginForm").classList.remove("hidden");
document.getElementById("notesApp").classList.add("hidden");
}
function showNotesApp() {
document.getElementById("loginForm").classList.add("hidden");
document.getElementById("notesApp").classList.remove("hidden");
}
// Check for existing token on load
window.addEventListener("load", () => {
let token = localStorage.getItem("token");
if (token) {
currentUser = { token };
showNotesApp();
loadNotes();
} else {
showLoginForm();
}
});
Best Practices Covered
✅ RESTful API design
✅ Input validation
✅ Error handling
✅ Authentication with tokens
✅ Data persistence
✅ Frontend-backend communication
✅ Code organization
✅ Security considerations
Learning Outcomes
After completing this project, you can:
- ✅ Build full-stack applications
- ✅ Create REST APIs with Express
- ✅ Handle authentication
- ✅ Persist data
- ✅ Connect frontend to backend
- ✅ Deploy applications
Congratulations! You've completed the entire JavaScript course.
What's Next?
After completing this course, explore:
- Advanced Topics: TypeScript, Design Patterns, Advanced Async
- Frontend Frameworks: React, Vue, Angular
- Databases: MongoDB, PostgreSQL
- Deployment: Heroku, AWS, Vercel
- Mobile Development: React Native
- Desktop Apps: Electron
Keep building, keep learning!