#100DaysOfMERN - Day 18

Subscribe to my newsletter and never miss my upcoming articles

✏ How Not To Make A POST Request

I said yesterday that I wanted to extend the functionality of my Book App, so that I can add new books with a POST request, and that new book would be added to a file in my backend folder.

I went to great lengths to make it work somehow. It's probably among the top 10 worst pieces of code that I've ever written. But I'm documenting what I did regardless, if only for amusement of my future self.


✏ Starter Code

Copying from yesterday for easier reference:

Frontend

App.js

import React, { useState, useEffect } from 'react';
import axios from 'axios';

const App = () => {

    const [books, setBooks] = useState([]);

    useEffect(() => {
        const url = 'http://localhost:3001/api/books';
        const getBooks = () => axios.get(url)
            .then(res => setBooks(res.data));
        getBooks();
    }, [])

    return (
        <div className="app">
            <header><h1>Books</h1></header>
            <div className="book-list">
                {books.map(book => <li key={book.id}>{book.title}</li>)}
            </div>
        </div>
    )
}

export default App

Backend

server.js

import express from 'express';
import cors from 'cors';
import books from './data/bookdata.js';

const app = express();

app.use(cors());

app.get('/', (request, response) => {
    response.send('<h1>Server is Serving</h1>')
})

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

app.listen(3001, console.log('server is serving'))

✏ Changes in the Frontend

If I want to POST data, I'd obviously need a form to submit a new book title, so adding an input and a submit button:

App.js

    return (
        <div className="app">
            { /* ... */}

            <form>
                <input type="text" id="input-title" value={title} onChange={handleInput}/>
                <button type="submit" onClick={handleSubmit}>submit</button>
            </form>
        </div>
    )

Now for the two event handlers. The onChange handler is simple, it controls the input and writes its value to state:

const [title, setTitle] = useState('');

const handleInput = e => setTitle(e.target.value)

The onSubmit handler will make use of the axios.post method, which takes a URL and an object with the submitted data, and clear the input field after that:

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

    function handleSubmit(e){
        e.preventDefault();

        const newBook = { title };
        axios.post(url, newBook);

        setTitle('');
    }

✏ Changes in the Backend

This was a real fight. First of all, I've set up a new route to handle POST requests. Theoretically, I should at least be able to log the request body, but nope:

server.js

app.post('/api/books', (request, response) => {
    console.log(request.body) // undefined
})

Turned out I need to add something at the top of my file, not 100% sure why, but that fixed the problem (and I was too desperate to make it work to have time to find an answer):

const app = express();

app.use(express.json());

Next, I started wondering how I'm supposed to get that new object into my bookdata.js file. I know that there's a package called json-server which apparently lets you do that, but I'd hardly learn how to do it myself when I just install it (also, I had tried to do that a couple of days ago in a different app and ran into errors and gave up). I looked at the source code though, and found that I'd need the File System API.

But first, the current state of my data file in data/bookdata.js:

const books = [{
        "id":"1",
        "title":"You Don't Know JS"
    },
    {
        "id":"2",
        "title":"How to Read a Book"
    },
    ...
]
export default books

✏ Writing and reading files with the File System API

fs is a Node Core Module, so it doesn't need to be installed, I can just import it:

import fs from 'fs';

It comes with a method fs.writeFileSync(path, content) that I used in my app.post callback:

const src = './data/bookdata.js';

app.post('/api/books', (request, response) => {
    const newBook = request.body;
    fs.writeFileSync(src, JSON.stringify(newBook))
})

That totally worked, in the sense that I was able to successfully write to the file. My book list was gone though, as was the export statement and everything else from that file. The only content in it was now:

{"title":"asdf"}

I'm not sure if I want to describe the painful hours that I spent trying to figure out how I can import content of a file without exporting it with an export statement, I tried everything, changed the structure of the file multiple times to make it a JSON object, changed the file extension to .json and back, and whatnot. The important part is that I finally came up with a solution: Instead of importing the data, I thought if I can use fs.writeFileSync, why not try fs.readFileSync?

So this is how I replaced the import statement:

server.js

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

//import books from './data/bookdata.js';

const src = './data/bookdata.js';
let books = JSON.parse(fs.readFileSync(src));

And this is the structure of the data file now, just an array of book objects:

bookdata.js

[
  {
    "id":"1",
    "title":"You Don't Know JS"
  },
  {
    "id":"2",
    "title":"How to Read a Book"
  },
  ...
]

✏ Updating the app.post function

This is just pure JS now. I know how to write a completely new file, and I know how to read the content (array) of a file to store it in my books variable. To give a book an id, I just take the highest id present in the array, and increment by 1. Then I add the new book to the list, and write the whole list back into the file.

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

    fs.writeFileSync(src, JSON.stringify(newList))
    response.json(newList)
})

As long as this is a test list with 10 or 100 entries, it shouldn't be a problem, but this is obviously not something you'd do with big data structures. I'll just take it as a proof of principle.

But my App still doesn't work as it should. First, I have to reload the page to see the updated list, and second, each time I add a new book, nodemon detects a change in my backend folder and restarts the server.


✏ Updating the Frontend again

My app.post function responds to the request by sending back the newList, but my Frontend doesn't update because the submit handler isn't updating the state of the books yet, I forgot to chain a .then:

    function handleSubmit(e){
        e.preventDefault();
        const newBook = { title };
        axios.post(url, newBook).then(res => setBooks(res.data))
        setTitle('');
    }

Now I can see the updated list after submitting a new entry, but my server is still mindlessly starting and restarting - but a quick research told me that I can configure nodemon with a nodemon.json file in the backend folder, and tell it to ignore changes in the data file.

✏ Configuring nodemon

A simple entry like this is enough. If there's more files to ignore, change the value of "ignore" to an array and list all files.

nodemon.json

{
  "ignore": "./data/bookdata.js"
}

This is still not perfect. If I manually make changes in the file, like deleting 50 test entries called "asdf", the server won't notice. Well I've told it to ignore changes in the file. It won't grab the new book list again when I reload the page. It already has the list stored in a variable and serves that old list again and again.

Nevertheless, I think I've learned a lot today. It's highly debatable if I'll ever need this in a real application, but I wanted to be able to submit a new book to my data file, and managed to do so. Presumably with horrible code, but WIN.


✏ Recap

I've learned

  • how to set up a POST route on a server
  • how to read and write to a local file with the File System API
  • how to configure nodemon to ignore certain files

✏ Next:

  • deleting books from the list

✏ 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

No Comments Yet