#100DaysOfMERN - Day 19

Subscribe to my newsletter and never miss my upcoming articles

✏ Fixing a Bug

The current functionality of my Boring Book App has a little bug, which I noticed after some further testing. I can add a new book to the list, and the entry gets successfully added to the file that holds the data, but when I add another book, it overwrites the previous entry.

This is the current code:

server.js

import express from 'express';
import cors from 'cors';
import fs from 'fs';

const app = express();
app.use(express.json());
app.use(cors());

// getting the data from a file
const src = './data/bookdata.js';
let books = JSON.parse(fs.readFileSync(src));

// handling GET requests
app.get('/api/books', (request, response) => {
    response.json(books)
})

// handling POST requests
app.post('/api/books', (request, response) => {
    const newBook = request.body;
    const newId = (Math.max(...books.map(b => +b.id)) + 1).toString();
    newBook.id = newId;

    // bug starts here
    const newList = books.concat(newBook);
    fs.writeFileSync(src, JSON.stringify(newList))
    response.json(newList)
})

The flaw lies in the POST callback: When the server starts, it reads the book list from the file once, then stores it in books. Any new entry gets written to the file, but the books variable doesn't get updated, so this:

const newList = books.concat(newBook);

will always append a new book to the original list, not the updated list. So, in order for this app to work properly, I'll have to make sure that the content of the file and the value of the books variable are always in sync.

Solution

It's very easy to fix this - instead of creating a newList, I just update the current one:

server.js

app.post('/api/books', (request, response) => {
    const newBook = request.body;
    const newId = (Math.max(...books.map(b => +b.id)) + 1).toString();
    newBook.id = newId;

    books.push(newBook);
    fs.writeFileSync(src, JSON.stringify(books))
    response.json(books)
})

It makes me wonder though if this is still a possible bug machine. Right now, I only have one function that updates the books variable and the file, but what if I add more functionality. I'll have to manually make sure that the variable and the file content are always synchronised, and it might be nothing but I find this a little unsettling.

To avoid that situation, I could get rid of the global books variable altogether, and read the file each time I do an operation on the list. But that's the opposite of DRY, and depending on the size of the list, it'll surely introduce performance issues.

I'll keep an eye on this, but I'll leave my code as it is for now, and move on to the next step: Adding buttons to delete a book from the list. But before I do so, I'll set up another route to get access to a specific entry on the list.


✏ Request Parameters

At the moment, I can see a list of all books if I go to http://localhost:3001/api/books/, because of this route:

app.get('/api/books', (request, response) => {
    response.json(books)
})

Now I also want to be able to get the data of one specific book through a URL like this:

  • http://localhost:3001/api/books/1 gives me the book with id=1
  • http://localhost:3001/api/books/2 gives me the book with id=2

It would be kind of stupid to hardcode a separate route for each book, and thankfully in programming we have variables that can serve as placeholders. On Day 17, I had already briefly touched the request object and its params property. It allows to set up a route with such a placeholder by adding a colon, and a request to http://localhost:3001/api/books/1 will then give access to the placeholder through request.params:

app.get('/api/books/:bookID', (request, response) => {
    console.log(request.params) // { bookID: '1' }
})

So now I can add some code to find the requested book in the list. If the book exists, I'll send its data, otherwise, I'll return a message that the ID is invalid:

app.get('/api/books/:bookID', (request, response) => {
    const id = request.params.bookID;
    const book = books.find(b => b.id === id);
    if (book) {
        response.json(book)
    } else {
        response.send('<h3>Invalid book ID</h3>')
    }
})

This route could be used to display a separate page through a "show details" button for each book in the Frontend, but I'm not even saving any details in the book data, the point is just to illustrate how to set up a route that leads to a specific entry.

I'll use a variation of it now to add a "delete book" button and make a DELETE request:


✏ DELETE request

First of all, adding a delete button to each book in the list in my App component:

App.js

return (
    <div className="app">
        {/* ... */}
        <div className="book-list">
            {books.map(b => {
                return (
                    <li key={b.id}>
                        <span>{b.title} (id: {b.id})</span>
                        <button
                            type="button"
                            value={b.id}
                            onClick={handleDelete}
                        >delete
                        </button>
                    </li>
                )
            })}
        </div>
        {/* ... */}
    </div>
)

The click handler will get the book ID from the button's value, add it to the base URL, and make a DELETE request:

const url = 'http://localhost:3001/api/books';

function handleDelete(e){
    const id = e.target.value;
    const bookUrl = `${url}/${id}`
    axios.delete(bookUrl).then(res => setBooks(res.data))
}

Moving over to the Backend - the server now needs a route to handle this request, which will look very similar to the GET route that I've already set up:

  • get the ID from request.params
  • filter the books array and reassign it
  • write the new array into my data file
  • and finally, return the updated list of books:
app.delete('/api/books/:bookID', (request, response) => {
    const id = request.params.bookID;
    books = books.filter(b => b.id !== id);
    fs.writeFileSync(src, JSON.stringify(books))
    response.json(books)
})

Tadaa! Piece of cake, once you get the hang of it 🎂


✏ Recap

I've learned

  • how to set up individual routes using request.params
  • how to make a DELETE request

✏ Next:

  • updating a specific entry with a PUT request

✏ Thanks for reading!

I do my best to thoroughly research the things I learn, but if you find any errors or have additions, please leave a comment below, or @ me on Twitter. If you liked this post, I invite you to subsribe to my newsletter. Until next time 👋


✏ Previous Posts

  • Day 1: Introduction, Node.js, Node.js in the terminal
  • Day 2: npm, node_modules, package.json and package-lock.json, local vs global installation of packages
  • Day 3: Create a React app without create-react-app, Webpack, Babel
  • Day 4: npx and cowsay
  • Day 5: npm vs. npx, npm audit, semantic versioning and update rules
  • Day 6: Call stack, event loop, JavaScript engine, JavaScript runtime
  • Day 7: Call stack and event loop in Node.js, setImmediate()
  • Day 8: setImmediate(), process.nextTick(), event loop phases in Node.js
  • Day 9: Network requests with XMLHttpRequest and callbacks
  • Day 10: Promises
  • Day 11: Network requests with XMLHttpRequest and Promises
  • Day 12: React Quiz App part 1
  • Day 13: React Hangman
  • Day 14: FullStackOpen course 1: details of GET and POST requests, request headers
  • Day 15: React Hangman: Trigger fetch with click event callback vs useEffect
  • Day 16: REST API and CRUD
  • Day 17: Boring Book App part 1: React Frontend, Express Backend, GET requests, CORS
  • Day 18: Boring Book App part 2: POST request, File System API

No Comments Yet