Sailing Through Transactions

An In-Depth Look at the Ultimate Transaction Tracking Application

Curiosity drove me to explore GraphQL and its perks. Having never interacted with it before, I was keen to find a seamless way of incorporating it into my React application. Grafbase, a database that promised a serverless backend for my application as well as a single endpoint for accessing data, proved to be an optimum route to take. My application of choice was a transaction tracking application that sought to address the problem of financial management and organization in an increasingly fast-paced and interconnected world with a view of increasing financial literacy in its users. I started by selecting React as my library of choice for my front end alongside the serverless backend provided by Grafbase. I was able to come up with a transaction tracking application appropriately named 'Transaction Tracker'.

Transaction tracker application image

This article aims to help you step onto the dance floor of technology, where React and GraphQL perform their graceful moves in harmony, with Grafbase as their stage.

Getting Started

Let's start by figuring out what exactly we want to make. To do this, we'll do a bit of planning to make sure we're on the same page about what we need. So, here's a simple breakdown of the key tasks our app should be able to handle:

  1. Adding transactions: We should be able to input new transactions into the app.

  2. Listing transactions: The app should show us a list of all the transactions we've added.

  3. Editing: If we make a mistake or need to change something, the app should let us easily edit the transactions.

  4. Deleting transactions: If we no longer need a transaction, the app should allow us to remove it.

With that, let's dive right in.

Prerequisites:

  • Basic knowledge of HTML, CSS, and JavaScript

  • Node.js and npm (Node Package Manager) installed on your computer

  • A code editor. eg. VS Code

Step 1: Setting Up the Project

First, let's set up the project by creating a new React app. Open your terminal and run the following commands:

npx create-react-app transaction-tracker

and...

npx grafbase init

By now, your file structure should be looking like this:

Step 2: Creating the App Component

Now, let's start building the transaction tracker's UI by modifying the src/App.js file. Replace the default code with the following:

import React from "react";

function App() {
  const transactions = [
    {
      id: 1,
      description: "Travel Expenses",
      amount: 10000,
      date: "04/21/2023",
      type: "Expense"
    },
    {
      id: 2,
      description: "Groceries",
      amount: 3500,
      date: "08/10/2023",
      type: "Expense"
    },
    {
      id: 3,
      description: "Salary",
      amount: 50000,
      date: "08/01/2023",
      type: "Revenue"
    },
    {
      id: 4,
      description: "Rent",
      amount: 12000,
      date: "08/05/2023",
      type: "Expense"
    }
  ];

  return (
    <div className="App">
      <h1 className="title">Transaction Tracker</h1>
      <div className="transaction-form">
        <input
          type="text"
          className="input"
          placeholder="Description"
        />
        <input
          type="number"
          className="input"
          placeholder="Amount"
        />
        <input
          type="date"
          className="input"
        />
        <select
          className="input"
        >
          <option value="expense">Expense</option>
          <option value="revenue">Revenue</option>
        </select>
        {editingTransaction ? (
          <div>
            <button className="update-button">
              Update Transaction
            </button>
            <button className="cancel-button">
              Cancel
            </button>
          </div>
        ) : (
          <button className="add-button" onClick={handleAddTransaction}>
            Add Transaction
          </button>
        )}
      </div>
      <div>
        <ul className="transaction-list">
          {transactions.map((transaction) => (
            <li
              key={transaction.id}
              className={`transaction-item ${
                transaction.type === "expense" ? "expense" : "revenue"
              }`}
            >
              <strong>{transaction.description}</strong> ${transaction.amount.toFixed(2)} ({transaction.date})
              <div className="button-group">
                <button className="edit-button">
                  Edit
                </button>
                <button className="delete-button">
                  Delete
                </button>
              </div>
            </li>
          ))}
        </ul>
        <p className="total">Total: ${totalAmount.toFixed(2)}</p>
      </div>
    </div>
}

export default App;

The code provided above gives our app a basic structure to work with. Here's what we can deduce from it:

  1. Storing Transactions: Right now, the transactions are directly written into the code. However, this isn't practical for the future. We need a better way to save and manage transactions.

  2. Important Fields: Our transactions have specific details that we want to keep track of an id, description, amount, date, and type. Thus, even in storage, these fields should be accommodated.

This is where Grafbase comes in!

To view the above changes on your browser, run the following commands on your terminal:

cd transactions-tracker
npm start

Now, you should be able to access your application on your browser via localhost:3000.

Step 3: Grafbase Schema and CLI

For this step, we shall be editing the grafbase/schema.graphql file. What we should be conscious of is that our Grafbase instance will be running locally on our computer. Replace the grafbase/schema.graphql code with the below code.

type Transaction @model {
  id: ID!
  description: String!
  amount: Float!
  date: String!
  type: String!
}

On your new or split terminal, run the following commands:

If you are using VS Code, to create a new terminal or split it, go to 'Terminal' on the top-most bar, select it and select the former or latter.

cd grafbase
npx grafbase dev

If everything is running right, you should get a response like in the one in the image below.

Just like that, Grafbase is running locally on our computer. To make requests to it, we shall be using the endpoint http://localhost:4000/graphql for the time being.

Additionally, a visit to http://localhost:4000 will present a playground similar to the one in the image below. This is one of the perks of using Grafbase - the ability to test queries without having to run entire applications.

Important

While I intended to use the localhost:4000/graphql endpoint to test out my application locally, I kept getting the "Unauthorized" error message. Having determined that I would require some sort of an API KEY, I opted to first deploy my grafbase/schema.graphql to Grafbase.

Consequently, these subsequent parts are going to address the deployment to Grafbase. Thereafter, we shall get back to our application's logic so consider this as a much-needed detour.

Step 4: GitHub

First of all, we need to host our application on GitHub. This step will be taken care of by the following commands which can either be run on our terminal or Git Bash. It is assumed that you have a GitHub account and have created a new repository. As such, the commands below will initialize an empty GitHub repository, add, commit and push all the files to your GitHub repository.

git init
git add .
git commit -m "The fetch, add, edit and delete transactions functionalities added"
git branch -M main
git remote add origin GitHub-Repository-URL
git push -u origin main

Step 5: Creating a Grafbase account

Our next stopover brings us to the creation of a Grafbase account. It is simple and quick so let's get right to it! Navigate to the Grafbase Website and click on the Sign Up button.

You will be navigated to the page below. I would recommend signing up with your GitHub account to ease the process of accessing your GitHub repository.

Most likely, an interface like the one below will pop up.

Just like that, our Grafbase account is created and we are ready for deployment.

Step 6: Deploying to Grafbase

There are two methods of deploying your schema to Grafbase. One, through Grafbase's official website and two, through our VS Code terminal. I shall address both.

  1. Deploying via GitHub

  • Mine has two projects already so it is similar to the one below. Yours will be empty but worry not, for we shall set up one right here. Click on Create Project.

  • Search for the repository which contains your application. Click on Import.

  • Name your project appropriately and click on Deploy.

  • Deployment might take a little while after which you will be navigated to the interface below. Our application, specifically the schema.graphql file is now deployed to Grafbase.

    If you get the 'failed to deploy' error during deployment or 'failed to find schema.graphql, consider making sure that your application, specifically the grafbase folder, is located at the root of the GitHub repository as in the image below. With that, deployment should be smooth enough.

  • Navigate to the connect tab to access your API URL and KEY. We shall be using them for our application.

  1. Deploying via the terminal

While one can deploy their Grafbase schema by linking their GitHub repository, I prefer the terminal way better - well, because I appreciate its ease and accessibility. This involves the login and creation of Grafbase projects all at the comfort of your terminal. The commands below navigate to the current Grafbase directory and initialize a Grafbase instance.

cd grafbase 
npx grafbase init

Since we had already created our Grafbase account, we are only required to log in for authentication. Afterward, we have to create a Grafbase project which will be deployed, automatically. The following commands take care of that.

npx grafbase login
npx grafbase create

To redeploy an existing project where there are changes or updates, run:

npx grafbase deploy

With that, your Grafbase project will have been created. Navigate to the connect tab to access the API URL and KEY in a pop-up.

Step 7: Using the Grafbase endpoint

This is probably a great time to copy the API URL and replace it in our project. The same goes for our API KEY. You may consider storing them in a .env file to ensure the security of your application. Update the env.local file with the variables below.

REACT_APP_MY_API_KEY= "API_KEY"
REACT_APP_MY_API_URL= "API_URL"

To access them in our application, we will have to install dotenv. Run the following command to install it.

npm i --save-dev dotenv

Step 8: Grafbase Pathfinder

Just before we move on to our application's code, I thought it wise to establish the queries that we will eventually use. The idea is that if the query works in Pathfinder, it should work in our application. To access the Pathfinder, you may choose to go to the Grafbase website way and go to the View Pathfinder tab. An alternative is using the terminal as discussed in Step 3.

  • Adding a Transaction

In this example, we are using the transactionCreate mutation to add a new transaction to our system. The input parameter allows us to provide the necessary details for the transaction, such as its description, amount, date, and type.

mutation TransactionCreate {
  transactionCreate(
    input: {
      description: "Roofing bill"
      amount: 34
      date: "2023-02-02"
      type: "expense"
    }
  ) {
    transaction {
      id
      description
      amount
      date
      type
    }
  }
}

Breaking Down the Mutation:

  • transactionCreate: This is the name of the mutation. In GraphQL, we name our mutations for clarity and ease of use.

  • input: The input parameter takes an object that holds the specific details of the transaction we want to create. In this case, we're creating a transaction with a description of "Roofing bill," an amount of $34, a date of February 2, 2023, and a type of "expense."

  • transaction: This field specifies what data we want to retrieve as a result of the mutation. Here, we are asking for the id, description, amount, date, and type of the newly created transaction.

  • Fetching a Transaction

In this demo, we are fetching our transactions. The query below worked for me and gave me the response above.

 {
      transactionCollection(first: 50) {
        edges {
          node {
            id
            description
            amount
            date
            type
          }
        }
      }
    }

Let's go through each part of the above query.

  1. transactionCollection(first: 50) represents a collection of transactions. The first argument specifies that you want to retrieve the first 50 transactions from this collection. This is a common pattern in GraphQL to limit the number of results returned.

  2. edges is a field that is used to access the edges of the transactionCollection. In GraphQL, edges are often used in connection with pagination, where each edge represents a data item.

  3. node is used to access the data within each edge. In GraphQL, nodes typically contain the actual data that you're interested in retrieving.

  4. Inside the node field, there are several subfields:

    • id: This subfield represents the unique identifier of a transaction.

    • description: This subfield represents the description of the transaction.

    • amount: This subfield represents the amount of the transaction.

    • date: This subfield represents the date of the transaction.

    • type: This subfield represents the type of the transaction (e.g., "expense" or "revenue").

When you execute this GraphQL query against a server that supports it, you'll receive a response containing the requested data for the first 50 transactions in the transactionCollection. The data will be structured in a way that mirrors the structure of the query, with an array of edges, and within each edge, an object containing the node data (including id, description, amount, date, and type). This kind of structured response allows you to efficiently retrieve and work with the specific data you need from the server.

Step 9: Fetching Transactions

In this step, we'll use the useEffect hook to fetch transactions from a GraphQL API. This is to ensure that transactions are only fetched in the initial render.

const fetchTransactions = async () => {
    try {
      const response = await fetch(process.env.REACT_APP_MY_API_URL, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          "x-api-key": API_KEY,
        },
        body: JSON.stringify({
          query: `
          {
            transactionCollection(first: 50) {
              edges {
                node {
                  id
                  description
                  amount
                  date
                  type
                }
              }
            }
          }
          `,
        }),
      });

      const result = await response.json();
      console.log("API Response:", result);

      if (result.data && result.data.transactionCollection) {
        const transactionsData = result.data.transactionCollection.edges.map(edge => edge.node);
        setTransactions(transactionsData);
        const highestId = Math.max(...transactionsData.map(item => item.id));
        setCurrentId(highestId + 1);
      }
    } catch (error) {
      console.error("Error fetching transactions:", error);
    }
  };
  1. const fetchTransactions = async () => {} defines an asynchronous function named fetchTransactions, which will be used to fetch transaction data from a GraphQL API.

  2. try { initiates a try-catch block, where the code inside the try block will attempt to execute, and any errors that occur will be caught and handled in the catch block.

  3. Inside the try block, a fetch request is made to your API URL

    • method: "POST" specifies that a POST request will be made.

    • headers specify the headers for the request, including the content type as JSON and an x-api-key header containing the API key.

    • body contains a GraphQL query as a JSON string. The query requests transaction data using the transactionCollection field, retrieving details such as id, description, amount, date, and type.

  4. const result = await response.json();: This line awaits the response from the fetch request parses the response body as JSON, and assigns the parsed result to the result variable.

  5. console.log("API Response:", result);: This logs the received API response to the console for debugging purposes.

  6. Inside the if statement, the code checks if the result contains valid data and if the transactionCollection field is present:

    • If both conditions are met, the code extracts the transaction data from the edges using the map function and stores it in the transactionsData array.

    • The setTransactions function is used to update the state with the fetched transaction data.

    • The code calculates the highest ID among the fetched transactions and increments it by 1, setting it as the currentId.

  7. } catch (error) {: This begins the catch block, where any errors that occurred during the try block execution will be caught and handled.

    • The code logs the error to the console for debugging purposes.

NB: Since we are using a single endpoint to access multiple versions of the same data, we use the POST method where we would have used the GET method in CRUD operations.

Step 10: Displaying Transactions

To display our transactions be it after fetching or adding them, we have to map through the transactions variable because our transactions are initially of type array.

        <ul className="transaction-list">
          {transactions.map((transaction) => (
            <li
              key={transaction.id}
              className={`transaction-item ${
                transaction.type === "expense" ? "expense" : "revenue"
              }`}
            >
              <strong>{transaction.description}</strong> ${transaction.amount.toFixed(2)} ({transaction.date})
              <div className="button-group">
                <button className="edit-button">
                  Edit
                </button>
                <button className="delete-button">
                  Delete
                </button>
              </div>
            </li>
          ))}
        </ul>

Step 11: Adding Transactions

Let's implement the functionality to add transactions. We shall be using the handleAddTransaction function.

const handleAddTransaction = async () => {
    if (description && amount && date) {
      try {
        const response = await fetch(process.env.REACT_APP_MY_API_URL, {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
            "x-api-key": API_KEY,
          },
          body: JSON.stringify({
            query: `
            mutation TransactionCreate($input: TransactionCreateInput!) {
              transactionCreate(input: $input) {
                transaction {
                  id
                  description
                  amount
                  date
                  type
                }
              }
            }
            `,
            variables: {
              input: {
                description,
                amount: parseFloat(amount),
                date,
                type,
              },
            },
          }),
        });

While the code may seem similar to that of fetching transactions, let's address the differences.

  1. Function Name and Purpose:

    • The function name is handleAddTransaction. This function is specifically designed to handle the addition of a new transaction.
  2. Conditional Check:

    • Before attempting to add a transaction, the code checks if the description, amount, and date values are all truthy (if (description && amount && date)). This ensures that all required fields have been provided before proceeding.
  3. Mutation Operation:

    • Instead of a query, this code is using a GraphQL mutation operation. Mutations are used to modify data on the server. In this case, the TransactionCreate mutation is used to create a new transaction.
  4. Mutation Variables:

    • The variables object is used to provide the necessary input data for the mutation. The input variable includes the description, amount, date, and type fields for the new transaction.
  5. Variables Placeholder:

    • In the query, $input is used as a variable placeholder. This variable will be replaced with the actual variables object provided in the request.

Step 12: Deleting Transactions

Now that we can fetch and add our transactions, let us work on deleting them.

const handleDeleteTransaction = async (transactionId) => {
    try {
      const response = await fetch(process.env.REACT_APP_MY_API_URL, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          "x-api-key": API_KEY,
        },
        body: JSON.stringify({
          query: `
            mutation TransactionDelete($id: ID!) {
              transactionDelete(by: { id: $id }) {
                deletedId
              }
            }
          `,
          variables: {
            id: transactionId,
          },
        }),
      });

      const result = await response.json();
      console.log("Delete Transaction Result:", result);

      if (result.data && result.data.transactionDelete) {
        const deletedId = result.data.transactionDelete.deletedId;
        const updatedTransactions = transactions.filter(transaction => transaction.id !== deletedId);
        setTransactions(updatedTransactions);

      }
    } catch (error) {
      console.error("Error deleting transaction:", error);    }
  };

The above code has the features explained below.

  • Function Name and Purpose:

The function name is handleDeleteTransaction. This function is designed to handle the deletion of a transaction.

  • Mutation Operation:

Similar to the previous code, this code also uses a GraphQL mutation operation (TransactionDelete) to delete a transaction. The mutation specifies that a transaction should be deleted based on the provided transactionId.

  • Mutation Variables:

The variables object is used to provide the transactionId for the mutation. This variable is used to identify the transaction to be deleted.

  • Response Handling:

After receiving the API response, the code checks if the result contains valid data and if the transactionDelete field is present.

If both conditions are met, the code extracts the deletedId from the response, which represents the ID of the deleted transaction.

The updatedTransactions array is created by filtering out the deleted transaction from the existing transactions array.

The state is updated with the updatedTransactions array using the setTransactions function.

  • Error Handling:

Similar to previous examples, the code includes error handling in a catch block to log errors and display an error toast if the deletion process encounters an issue.

Step 13: Editing Transactions

For editing transactions, the following steps can be followed:

  1. handleEditTransaction Function:
 const handleEditTransaction = (transaction) => {
    setDescription(transaction.description);
    setAmount(transaction.amount.toString());
    setDate(transaction.date);
    setType(transaction.type);
    setEditingTransaction(transaction);
  };
  • This function is triggered when the "Edit" button is clicked on a transaction item in the UI.

  • It takes a transaction object as an argument, which represents the transaction being edited.

  • The function uses the individual state setter functions (setDescription, setAmount, setDate, setType, setEditingTransaction) to update the local state values with the properties of the transaction object. This pre-fills the input fields with the existing transaction data for editing.

  1. handleUpdateTransaction Function:
const handleUpdateTransaction = async () => {
    if (description && amount && date && editingTransaction) {
      try {
        const updatedTransaction = {
          description,
          amount: parseFloat({amount}),
          date,
          type,
        };

        const response = await fetch(process.env.REACT_APP_MY_API_URL, {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
            "x-api-key": API_KEY,
          },
          body: JSON.stringify({
            query: `
            mutation TransactionUpdate($by: TransactionByInput!, $input: TransactionUpdateInput!) {
              transactionUpdate(by: $by, input: $input) {
                transaction {
                  id
                  description
                  amount
                  date
                  type
                }
              }
            }
            `,
            variables: {
              by: { id: editingTransaction.id },
              input: updatedTransaction,
            },
          }),
        });

        const result = await response.json();
        console.log("Update Transaction Result:", result);

        if (result.data && result.data.transactionUpdate) {
          const updatedTransactionData = result.data.transactionUpdate.transaction;
          const updatedTransactions = transactions.map(transaction =>
            transaction.id === updatedTransactionData.id ? updatedTransactionData : transaction
          );
          setTransactions(updatedTransactions);

          setDescription("");
          setAmount("");
          setDate("");
          setType("expense");
          setEditingTransaction(null);

        }
      } catch (error) {
        console.error("Error updating transaction:", error);
      }
    }
  };
  • This function is triggered when the "Update" button is clicked after editing a transaction.

  • It checks if all required fields are filled (description, amount, date) and if an editingTransaction is set (indicating an edit operation).

  • It creates an updatedTransaction object with the edited values, preparing data for the mutation.

  • It sends a mutation (TransactionUpdate) to update the transaction data on the server.

  • After receiving the response, it updates the UI state with the edited transaction and clears the input fields.

  1. Transaction List Rendering:
        <ul className="transaction-list">
          {transactions.map((transaction) => (
            <li
              key={transaction.id}
              className={`transaction-item ${
                transaction.type === "expense" ? "expense" : "revenue"
              }`}
            >
              <strong>{transaction.description}</strong> ${transaction.amount.toFixed(2)} ({transaction.date})
              <div className="button-group">
                <button className="edit-button" onClick={() => handleEditTransaction(transaction)}>
                  Edit
                </button>
                <button className="delete-button" onClick={() => handleDeleteTransaction(transaction.id)}>
                  Delete
                </button>
              </div>
            </li>
          ))}
        </ul
  • This JSX code fragment renders a list of transactions using the map function.

  • For each transaction, it displays the description, amount, and date properties in the UI.

  • It renders "Edit" and "Delete" buttons for each transaction, which invoke the handleEditTransaction and handleDeleteTransaction functions respectively.

Step 14: Styling the App

To make the app visually appealing, we'll apply some basic styling. You can refer to the provided App.css styles below:

.App {
  font-family: Arial, sans-serif;
  text-align: center;
  margin: 0 auto;
  max-width: 800px;
  padding: 20px;
  background-color: #f5f5f5;
}

h1,
.title {
  font-size: 24px;
  margin-bottom: 20px;
}

.transaction-form {
  display: flex;
  flex-direction: column;
  margin-bottom: 20px;
}

.input {
  padding: 10px;
  margin: 5px 0;
  border: 1px solid #ccc;
  border-radius: 4px;
}

.add-button,
.update-button,
.cancel-button {
  padding: 10px;
  margin: 5px;
  background-color: #007bff;
  color: #fff;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.add-button:hover,
.update-button:hover,
.cancel-button:hover {
  background-color: #0056b3;
}

.error {
  color: red;
  margin-top: 10px;
}

.transaction-list {
  list-style: none;
  padding: 0;
  margin: 0;
}

.transaction-item {
  background-color: #fff;
  border: 1px solid #ccc;
  padding: 10px;
  margin: 5px 0;
  border-radius: 4px;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.transaction-content {
  display: flex;
  align-items: center;
}

.transaction-description {
  font-weight: bold;
}

.transaction-amount {
  margin-left: 15px;
  font-size: 18px;
}

.transaction-item button {
  padding: 5px 10px;
  margin-left: 10px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.transaction-item button.edit-button {
  background-color: #007bff;
  color: #fff;
}

.transaction-item button.edit-button:hover {
  background-color: #0056b3;
}

.transaction-item button.delete-button {
  background-color: #dc3545;
  color: #fff;
}

.transaction-item button.delete-button:hover {
  background-color: #c82333;
}

.expense {
  background-color: #ffc4c4;
}

.revenue {
  background-color: #c1f0c1;
}

.total {
  margin-top: 10px;
  font-size: 18px;
  font-weight: bold;
}

Step 15: Test the app

With that, we can now see what we have come up with. Isn't it amazing! The full code can be accessed on my GitHub page where features such as user feedback with Toastify have been added.

Below is a video of the application in action.

If we want to view the metrics of our API and the number of requests sent to it, we can simply visit the Grafbase website and navigate to our previously created project. A sample of that can be accessed in the image below.

Conclusion

A lot can be done in improving this application. A good example is the addition of a registration and login page. Other improvements to the code might include the incorporation of components to make the code reusable as well as using Apollo and Relay to make the code more readable and clean. Perhaps, you may consider these additional features as my challenge to you even as you implement your version of this transaction tracker.

The above was my submission for the Grafbase Hackathon in conjunction with Hashnode. Thank you, guys, for reading my article! Feedback in the form of questions and comments is welcome.