DEV Community

Cover image for Full-Stack React & Node.js - CRUD
Dave
Dave

Posted on

Full-Stack React & Node.js - CRUD

Let's wrap things up.

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

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,
}
Enter fullscreen mode Exit fullscreen mode

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

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,
}
Enter fullscreen mode Exit fullscreen mode

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

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
Enter fullscreen mode Exit fullscreen mode

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:

app.use(bodyParser.json());
Enter fullscreen mode Exit fullscreen mode

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:

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;
Enter fullscreen mode Exit fullscreen mode

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

REACT_APP_URL_API=http://localhost:4011/
Enter fullscreen mode Exit fullscreen mode

Create useFetch.js:

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
Enter fullscreen mode Exit fullscreen mode

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:

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;
Enter fullscreen mode Exit fullscreen mode

In react-client Create TableList.js

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;
Enter fullscreen mode Exit fullscreen mode

In react-client Create table-list.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;
}
Enter fullscreen mode Exit fullscreen mode

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

In react-client Create NoteList.js

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;
Enter fullscreen mode Exit fullscreen mode

This uses TableList.js to list out Notes.

Finally, change App.js to:

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;
Enter fullscreen mode Exit fullscreen mode

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

Discussion (0)