Photo by Glenn Carstens-Peters on Unsplash
Sailing Through Transactions
An In-Depth Look at the Ultimate Transaction Tracking Application
Table of contents
- Getting Started
- Step 1: Setting Up the Project
- Step 2: Creating the App Component
- Step 3: Grafbase Schema and CLI
- Step 4: GitHub
- Step 5: Creating a Grafbase account
- Step 6: Deploying to Grafbase
- Step 7: Using the Grafbase endpoint
- Step 8: Grafbase Pathfinder
- Step 9: Fetching Transactions
- Step 10: Displaying Transactions
- Step 11: Adding Transactions
- Step 12: Deleting Transactions
- Step 13: Editing Transactions
- Step 14: Styling the App
- Step 15: Test the app
- Conclusion
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'.
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:
Adding transactions: We should be able to input new transactions into the app.
Listing transactions: The app should show us a list of all the transactions we've added.
Editing: If we make a mistake or need to change something, the app should let us easily edit the transactions.
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:
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.
Important Fields: Our transactions have specific details that we want to keep track of an
id
,description
,amount
,date
, andtype
. 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.
- Deploying via GitHub
- Navigate to the Dashboard tab of the Grafbase website.
- 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.
- 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
: Theinput
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 theid
,description
,amount
,date
, andtype
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.
transactionCollection(first: 50)
represents a collection of transactions. Thefirst
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.edges
is a field that is used to access the edges of thetransactionCollection
. In GraphQL, edges are often used in connection with pagination, where each edge represents a data item.node
is used to access the data within each edge. In GraphQL, nodes typically contain the actual data that you're interested in retrieving.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);
}
};
const fetchTransactions = async () => {}
defines an asynchronous function namedfetchTransactions
, which will be used to fetch transaction data from a GraphQL API.try {
initiates a try-catch block, where the code inside thetry
block will attempt to execute, and any errors that occur will be caught and handled in thecatch
block.Inside the
try
block, afetch
request is made to your API URLmethod: "POST"
specifies that a POST request will be made.headers
specify the headers for the request, including the content type as JSON and anx-api-key
header containing the API key.body
contains a GraphQL query as a JSON string. The query requests transaction data using thetransactionCollection
field, retrieving details such asid
,description
,amount
,date
, andtype
.
const result = await response.json();
: This line awaits the response from thefetch
request parses the response body as JSON, and assigns the parsed result to theresult
variable.console.log("API Response:", result);
: This logs the received API response to the console for debugging purposes.Inside the
if
statement, the code checks if theresult
contains valid data and if thetransactionCollection
field is present:If both conditions are met, the code extracts the transaction data from the
edges
using themap
function and stores it in thetransactionsData
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
.
} catch (error) {
: This begins thecatch
block, where any errors that occurred during thetry
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.
Function Name and Purpose:
- The function name is
handleAddTransaction
. This function is specifically designed to handle the addition of a new transaction.
- The function name is
Conditional Check:
- Before attempting to add a transaction, the code checks if the
description
,amount
, anddate
values are all truthy (if (description && amount && date)
). This ensures that all required fields have been provided before proceeding.
- Before attempting to add a transaction, the code checks if the
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.
- 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
Mutation Variables:
- The
variables
object is used to provide the necessary input data for the mutation. Theinput
variable includes thedescription
,amount
,date
, andtype
fields for the new transaction.
- The
Variables Placeholder:
- In the query,
$input
is used as a variable placeholder. This variable will be replaced with the actualvariables
object provided in the request.
- In the query,
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:
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 thetransaction
object. This pre-fills the input fields with the existing transaction data for editing.
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 aneditingTransaction
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.
- 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
, anddate
properties in the UI.It renders "Edit" and "Delete" buttons for each transaction, which invoke the
handleEditTransaction
andhandleDeleteTransaction
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.