[Node] To Do List Website w/ Minimum Functions

·

4 min read

[Node] To Do List Website w/ Minimum Functions

Directory Structure

  • /assets: all static files (FE) stored here

  • /models: all DB schemas stored here

  • /routes: all router middlewares stored here

File Details

app.js

const express = require("express");

const db = require('./models/index.js');
const todosRouter = require("./routes/todos.router.js");

const app = express();

app.use("/api", express.json(), todosRouter);
app.use(express.static("./assets")); // middleware for static files

app.listen(8080, () => {
    console.log("Server is running!");
});

models/index.js

const mongoose = require("mongoose");

mongoose
    .connect("mongodb://localhost:27017/todo-demo", {
        useNewUrlParser: true,
        useUnifiedTopology: true,
    })
    .then((value) => console.log("MongoDB connection succeeded."))
    .catch((reason) => console.log("MongoDB connection failed."));

const db = mongoose.connection;
db.on("error", console.error.bind(console, "connection error:"));

module.exports = db;

models/todo.js

const mongoose = require("mongoose");

const TodoSchema = new mongoose.Schema({
    value: String,
    doneAt: Date,
    order: Number,
});

// virtual item (todoId) can be only viewed on the mongoose
// the actual mongoDB (on Studio 3T) doesn't store this item
TodoSchema.virtual("todoId").get(function () {
    return this._id.toHexString();
});
TodoSchema.set("toJSON", { virtuals: true });

module.exports = mongoose.model("Todo", TodoSchema);

Virtuals

In the mongoose library, virtuals are used to add a "virtual" column to the data when querying MongoDB, even though the actual column does not exist in the database. This allows for easier data manipulation or usage by adding additional computed properties.

The Schema.set("toJSON", {virtuals: true}) configuration sets mongoose to return virtual values as part of the JSON representation of the Schema when converting data to JSON.

When comparing data from Studio 3T and mongoose, you can observe that the virtual todoId value, which is not actually present in MongoDB, is returned when querying with mongoose. The todoId virtual is configured to return this._id, which is the ObjectId of the Todo Schema, allowing it to be accessed as if it were an actual column in the data.

  • Studio 3T

  • mongoose

routes/todos.router.js

router.post("/todos", async (req, res) => {
    const { value } = req.body;
    const maxOrderByUserId = await Todo.findOne({}).sort("-order").exec(); // sort descending by order key

    // increment order by 1 if exists, otherwise, set as 1
    const order = maxOrderByUserId ? maxOrderByUserId.order + 1 : 1;

    // save(): create instance first, do something with the value, and save
    const todo = new Todo({ value, order });
    await todo.save();

    res.send({ todo });
});

exec()

It's generally recommended to use .exec() at the end of a Mongoose query in Node.js when you want to execute the query and retrieve the results. The .exec() method is used to return a Promise that resolves with the query result, which allows you to handle any errors that may occur during the query execution.

In the example you provided, const maxOrderByUserId = await Todo.findOne({}).sort("-order").exec(), the .exec() method is used after chaining the .sort() method, which sorts the result of the findOne() query by the "order" field in descending order. The await keyword is used to wait for the query to complete and return the result, which is then stored in the maxOrderByUserId variable.

router.patch("/todos/:todoId", async (req, res) => {
    const { todoId } = req.params;
    const { order, done, value } = req.body; // three events on FE

    // validation: check if id exists
    const currentTodo = await Todo.findById(todoId);
    if (!currentTodo) {
        return res
            .status(400)
            .json({ errorMessage: "data doesn't exist." });
    }

    // UPDATE
    // 1. change order (FE: user clicks up/down button)
    if (order) {
        const targetTodo = await Todo.findOne({ order }).exec();
        if (targetTodo) {
            targetTodo.order = currentTodo.order;
            await targetTodo.save();
        }
        currentTodo.order = order;
    } 
    // 2. change todo content (FE: user change the value in field)
    else if(value) {
        currentTodo.value = value;
    } 
    // 3. change if todo is completed (FE: user clicks the checkbox)
    else if(done !== undefined) {
        currentTodo.doneAt = done ? new Date() : null; 
    }

    await currentTodo.save();

    res.send({});
});

Methods for MongoDB

save()

https://stackoverflow.com/questions/38290684/mongoose-save-vs-insert-vs-create

The .save() is an instance method of the model, while the .create() is called directly from the Model as a method call, being static in nature, and takes the object as a first parameter.

// method 1 create()
  const newTour = await Tour.create(req.body);

// method 2 save()
const newTour = new Tour(req.body);
await newTour.save(); // for POST

targetTodo.order = currentTodo.order;
await targetTodo.save(); // for PATCH

remove()

DELETE is idempotent, so we do not need to check if the key/id exists for the input key/id.