Full-Stack React & Node.js - CRUD
Tags: react, node, database, webdev
Published: 2022-06-30 18:39:10

---
series: Simply Learn Full-Stack React & Node.js
---

Let's wrap things up.

In folder **node-server** edit **note.model.js** to:

```javascript
const { prisma } = require("./db")

async function getNotes() {
  return prisma.note.findMany()
}

async function getNote(id) {
  return prisma.note.findUnique({ where: { id } })
}

async function createNote(
  note
) {
  return prisma.note.create({
    data: note
  })
}

async function updateNote(
  id, note
) {
  return prisma.note.update({
    data: note,
    where: {
      id
    }
  })
}

async function deleteNote(
  id
) {
  return prisma.note.delete({
    where: {
      id
    }
  })
}

module.exports = {
  getNotes,
  getNote,
  createNote,
  updateNote,
  deleteNote,
}
```

In folder **node-server** edit **note.controller.js** to:

```javascript
const authorRepo = require('../models/author.model');
const noteRepo = require('../models/note.model');

async function getNotes(req, res) {
  const notes = await noteRepo.getNotes();

  res.json({
    notes
  });
}

async function getNote(req, res) {
  const {id} = req.params;
  const note = await noteRepo.getNote(id);
  const { authorId, ...noteRest } = note;
  const { username } = await authorRepo.getAuthor(authorId);

  res.json({ note: {
      ...noteRest,
      author: username
    }
  });
}

async function retrieveOrCreateAuthor(username) {
  let author = await authorRepo.getAuthorByName(username);
  if (author === null) {
    author = await authorRepo.createAuthor({
      username
    })
  }

  return author
}

async function postNote(req, res) {
  const {body} = req;
  const {title, content, author, lang, isLive, category} = body;

  try {
    const noteAuthor = await retrieveOrCreateAuthor(author);

    const note = await noteRepo.createNote({
      title,
      content,
      lang,
      isLive,
      category,
      authorId: noteAuthor.id
    })

    res
      .status(200)
      .json({
        note
      })
  } catch (e) {
    console.error(e);
    res.status(500).json({error: "Something went wrong"})
  }
}

async function putNote(req, res) {
  const {body} = req;
  const {id, title, content, author, lang, isLive, category} = body;

  try {
  const noteAuthor = await retrieveOrCreateAuthor(author);
  const note = await noteRepo.updateNote(id, {
    title,
    content,
    lang,
    isLive,
    category,
    authorId: noteAuthor.id
  })

  res
    .status(200)
    .json({
      note
    })
  } catch (e) {
    console.error(e);
    res.status(500).json({error: "Something went wrong"})
  }
}

async function deleteNote(req, res) {
  const {body} = req;
  const {id} = body;

  try {
    await noteRepo.deleteNote(id)

    res
      .status(200).send()
  } catch (e) {
    console.error(e);
    res.status(500).json({error: "Something went wrong"})
  }
}

module.exports = {
  getNotes,
  getNote,
  postNote,
  putNote,
  deleteNote,
}
```

In **node-server** edit **routes/index.js** to:

```javascript
const express = require('express');
const noteRouter = express.Router();
const noteController = require('../controllers/note.controller');

noteRouter.get('/', noteController.getNotes);
noteRouter.get('/:id', noteController.getNote);
noteRouter.post('/', noteController.postNote);
noteRouter.put('/', noteController.putNote);
noteRouter.delete('/', noteController.deleteNote);

const routes = app => {
  app.use('/note', noteRouter);
};

module.exports = routes
```

Server side we now have all the operations we need for the basic CRUD operations.

> Create, Read, Update, Delete

Try running the client and server now. If you click the submit button on the form you'll notice two problems: first the form doesn't respond, you could click over and over and not know if anything's happened. Second, if you look at the server console you'll notice an error.

> Argument isLive: Got invalid value 'true' on prisma.createOneNote. Provided String, expected Boolean.

`isLive` is a boolean but is being sent to Prisma as a string.

In **node-server** **index.js** we are using:

```javascript
app.use(bodyParser.json());
```

This does indeed retrieve the correct types during parsing, so the problem must be in the client.  When we gather up the input control data in **Form.js** in the `onSubmit` handler we are using `input.value` which always returns a string.

Edit **Form.js** to:

```javascript
import React, {useState} from 'react';
import InputLabel from "./InputLabel";
import {isEmptyString, isNullOrUndefined, titleFromName} from "./strings";
import './form.css'

const Form = ({entity, onSubmitHandler, onDeleteHandler}) => {
  const [isSubmitting, setIsSubmitting] = useState(false);

  return (
    <form onSubmit={e => {
      setIsSubmitting(true);
      const form = e.target;
      const newEntity = Object.values(form).reduce((obj, field) => {
        const {name} = field;

        if (!isEmptyString(name)) {
          switch (typeof entity[name]) {
            case "number":
              obj[name] = field.valueAsNumber;
              break;
            case "boolean":
              obj[name] = field.value === 'true';
              break;
            default:
              obj[name] = field.value
          }
        }

        return obj
    }, {})
      onSubmitHandler(newEntity);

      e.stopPropagation();
      e.preventDefault()
    }}>
      <fieldset
        disabled={isSubmitting}
      >
      {
        Object.entries(entity).map(([entityKey, entityValue]) => {
          if (entityKey === "id") {
            return <input
              type="hidden"
              name="id"
              key="id"
              value={entityValue}
            />
          } else {
            return <InputLabel
              id={entityKey}
              key={entityKey}
              label={titleFromName(entityKey)}
              type={
                typeof entityValue === "boolean"
                  ? "checkbox"
                  : "text"
              }
              value={entityValue}
            />
          }
        })
      }
      </fieldset>
      <button
        type="submit"
        disabled={isSubmitting}
      >
        {
          isSubmitting ? 'Submitting' : 'Submit'
        }
      </button>
      {
        onDeleteHandler && !isNullOrUndefined(entity.id) && <button
          disabled={isSubmitting}
          onClick={() => {
            setIsSubmitting(true);
            onDeleteHandler(entity.id)
          }}
        >
          Delete
        </button>
      }
    </form>
  );
};

export default Form;
```

Changes:

1. We wrap our input controls with a fieldset tag, allowing us to disable all controls when the user clicks "Submit"
2. We use a switch statement to parse the input value so it matches the type of the original entity we use to build the form.

If you try saving a form again you'll notice the bug is fixed.

Before we implement the rest of the CRUD operations a small refactor is needed. In **react-client**, create .env.development

```text
REACT_APP_URL_API=http://localhost:4011/
```
Create **useFetch.js**:

```javascript
import {useState, useEffect} from "react";

export const getUrl = url => new URL(url, process.env.REACT_APP_URL_API).toString();

function useFetch(url, skip) {
  const [data, setData] = useState({});

  useEffect( () => {
    const abortController = new AbortController();

    async function fetchData() {
      const fullUrl = getUrl(url);
      console.log('Fetching from: ' + fullUrl);
      try {
        const response = await fetch(fullUrl, {
          signal: abortController.signal,
        });

        if (response.ok) {
          console.log('Response received from server and is ok!')
          const res = await response.json();

          if (abortController.signal.aborted) {
            console.log('Abort detected, exiting!')
            return;
          }

          setData(res)
        }
      } catch(e) {
        console.log(e)
      }
    }

    !skip && fetchData()

    return () => {
      console.log('Aborting GET request.')
      abortController.abort();
    }
  }, [url, setData, skip])

  return data
}

export default useFetch
```

Currently our form can only add new notes, not edit. We need to do a few things:

1. List all notes
2. Edit a note
3. Add a note
4. Delete a note

Refactor **AddEditNote.js** to:

```jsx
import React from 'react';
import {useParams, useNavigate} from "react-router-dom";
import RenderData from "./RenderData";
import Form from './Form';
import useFetch, {getUrl} from "./useFetch";
import {isNullOrUndefined} from "./strings";

const AddEditNote = () => {
  const {noteId} = useParams();
  const {note = {
    title: '',
    content: '',
    lang: '',
    isLive: false,
    category: '',
    author: '',
  }} = useFetch('note/' + noteId, isNullOrUndefined(noteId));
  const navigate = useNavigate();

  return (
    <div>
      <RenderData
        data={note}
      />
      <Form
        entity={note}
        onSubmitHandler={async newNote => {
          console.log({newNote})
          const response = await fetch(getUrl('note'), {
            method: isNullOrUndefined(newNote.id) ? 'POST' : 'PUT',
            body: JSON.stringify(newNote),
            headers: {
              'Content-Type': 'application/json'
            }
          });

          if (response.ok) {
            await response.json()
            navigate('/note-list')
          }
        }}
        onDeleteHandler={async (id) => {
          if (!isNullOrUndefined(id)) {
            await fetch(getUrl('note'), {
              method: 'DELETE',
              body: JSON.stringify({id}),
              headers: {
                'Content-Type': 'application/json'
              }
            });

            navigate('/note-list')
          }
        }}
      />
    </div>
  );
};

export default AddEditNote;
```

In **react-client** Create **TableList.js**

```jsx
import React from 'react';
import {titleFromName} from './strings';
import './table-list.css';

const TableList = ({
                     data,
                     title,
                     onClickHandler,
                     idField = 'id',
                     fieldFormatter = {},
                   }) => {
  if (!data || data.length === 0) {
    return null
  }
  const firstRow = data[0];
  const dataColumnNamesToRender = Object.getOwnPropertyNames(firstRow)
    .filter(propName => propName !== idField);

  const headerRow = dataColumnNamesToRender.map((propName, i) => <th
    key={i}
  >
    {
      titleFromName(propName)
    }
  </th>);

  return (
    <table>
      <caption>
        {
          title
        }
      </caption>
      <thead>
      <tr>
        {
          headerRow
        }
      </tr>
      </thead>
      <tbody>
      {
        data.map((dataRow, i) => <tr
          key={i}
          onClick={() => onClickHandler && onClickHandler(dataRow[idField])}
        >
          {
            dataColumnNamesToRender.map((dataColumnName, i) => <td
              key={i}
            >
              {
                (fieldFormatter[dataColumnName] ?? (v => v))(dataRow[dataColumnName], dataRow)
              }
            </td>)
          }
        </tr>)
      }
      </tbody>
    </table>
  );
};

export default TableList;
```

In **react-client** Create **table-list.css**

```css
table {
    margin: 12px;
    border-collapse: collapse;
}

th {
    color: white;
    padding: 8px;
    background-color: #444;
}

td {
    border-bottom: 1px solid #ddd;
    padding: 12px;
}

td a,
td a:visited {
    color: black;
}

td:not(:last-child) {
    border-left:1px solid #ccc;
    border-right: 1px solid #ccc;
}

tr:nth-child(even) {
    background-color: #f1f1f1;
}
```

Just like our generic Form component, this is a generic data list component.

In **react-client** Create **NoteList.js**

```jsx
import React from 'react';
import TableList from "./TableList";
import {Link} from "react-router-dom";
import useFetch from "./useFetch";

const NoteList = () => {
  const {notes} = useFetch('note')

  return (
    <TableList
      data={notes}
      fieldFormatter={{
        title: (title, dataRow) => [
          <Link
            to={`/edit-note/${dataRow.id}`}
            key='1'
          >
            edit
          </Link>,
          <span key="2">
            &nbsp;{
              title
            }
          </span>
        ],
        dateCreated: date => new Date(date).toLocaleString()
      }}
    />
  );
};

export default NoteList;
```

This uses TableList.js to list out Notes.

Finally, change **App.js** to:

```jsx
import {
  Link,
  HashRouter as Router,
  Routes,
  Route,
} from 'react-router-dom';
import AddEditNote from "./AddEditNote";
import NoteList from "./NoteList";
import './App.css';

function App() {
  return (
      <div className="App">
        <Router>
          <Routes>
            <Route exact path="/" element={
              <ul>
                <li>
                  <Link to="/note-list">List Notes</Link>
                </li>
                <li>
                  <Link to="/edit-note">Create Note</Link>
                </li>
              </ul>
            }/>
            <Route path="/note-list" element={<NoteList/>}/>
            <Route path="/edit-note" element={<AddEditNote/>}/>
            <Route path="/edit-note/:noteId" element={<AddEditNote/>}/>
          </Routes>
        </Router>
      </div>
  );
}

export default App;
```

If you run this now, you have all basic CRUD operations working.

Congrats, full-stack.

This app is missing a few things: form validation and date handling, also dropdown lists; however, these should be easy things to add...

Code repo: [Github Repository](https://github.com/neohed/node-react-stack)