Project 3 - A Notetaking Web App

Project Overview

Build a complete full-stack notetaking application with:

Functional Requirements

Technical Requirements

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:


Congratulations! You've completed the entire JavaScript course.


What's Next?

After completing this course, explore:

Keep building, keep learning!