Mini Cashier App with ReactJS, TypeScript, Spring Boot, ExpressJS, and GraphQL

Building a Fullstack Mini Cashier App with ReactJS, TypeScript, Spring Boot, ExpressJS, and GraphQL

D2Y MVN
11 min readOct 10, 2024

Hello readers!

Welcome to my article where I will discuss how to build a mini cashier application using several technologies. If you are a developer interested in learning how to integrate various technologies, from frontend to backend, and how to leverage both relational and non-relational databases, then this article is perfect for you. I will guide you through the steps of developing a mini cashier application using ReactJS, Java Spring Boot, NodeJS (Express), GraphQL, and many other technologies.

The topics I will cover include how we can manage product, customer, and transaction data with a modern system, from setting up databases to implementing caching with Redis, and full integration from backend to frontend.

Great! Let’s break down the steps for creating the APIs using Spring Boot, ExpressJS with TypeScript, Redis, GraphQL, and the integration with ReactJS with TypeScript, based on the schema you provided in the image. The database design appears to have three main entities: Customers, Transactions, and Product, all of which are linked through qr_code and rf_id as the foreign keys.

Step 1: Create APIs using Spring Boot

In this step, we’ll build REST APIs for handling Customers, Product, and Transactions using Spring Boot and PostgreSQL for data storage.

1.1. Set Up Spring Boot Project

  • Create a new Spring Boot project with dependencies:
  • Spring Web for building REST APIs
  • Spring Data JPA for database access
  • PostgreSQL for relational database support
  • Lombok for generates boilerplate code

1.2. Create Entity Classes

// Customer
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity
@Table(name = "customers")
public class Customer {
@Id
private String qrCode;

@Column(nullable = false)
private String name;

@Column(nullable = false)
private int wallet;

}

// Product
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity
@Table(name = "products")
public class Product {
@Id
private String rfId;

@Column(nullable = false)
private String productName;

@Column(nullable = false)
private int price;

}

// Transaction
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity
@Table(name = "transactions")
public class Transaction {
@Id
private String id;

@ManyToOne
@JoinColumn(name = "qr_code", referencedColumnName = "qrCode", nullable = false)
private Customer customer;

@ManyToOne
@JoinColumn(name = "rf_id", referencedColumnName = "rfId", nullable = false)
private Product product;

@Column(nullable = false)
private int price;

@Column
private int totalPrice;

@Column
private LocalDateTime date;

}

1.3. Create Repository Interfaces

Each entity will have its corresponding repository interface for CRUD operations.

1.4. Create Controller Classes

Each controller will handle the respective API endpoints for Customers, Products, and Transactions.

1.5. Create Services

Every service will be responsible for its own logic and for storing data in a database or other storage.

CustomerService:

@Service
@RequiredArgsConstructor
public class CustomerService {
private final CustomerRepository customerRepository;

public List<CustomerDTO> getAllCustomers() {
return customerRepository.findAll().stream()
.map(this::mapToDTO)
.collect(Collectors.toList());
}

public CustomerDTO getCustomerDetails(String qrCode) {
Customer customer = customerRepository.findById(qrCode)
.orElseThrow(() -> new ResourceNotFoundException("Customer not found"));
return mapToDTO(customer);
}

private CustomerDTO mapToDTO(Customer customer) {
return CustomerDTO.builder()
.qrCode(customer.getQrCode())
.name(customer.getName())
.wallet(customer.getWallet())
.build();
}
}

ProductService:


@Service
@RequiredArgsConstructor
public class ProductService {

private final ProductRepository productRepository;

public List<ProductDTO> getAllProducts() {
return productRepository.findAll().stream()
.map(this::mapToDTO)
.collect(Collectors.toList());
}

public ProductDTO getProductDetails(String rfId) {
Product product = productRepository.findById(rfId)
.orElseThrow(() -> new ResourceNotFoundException("Product not found"));
return mapToDTO(product);
}

private ProductDTO mapToDTO(Product product) {
return ProductDTO.builder()
.rfId(product.getRfId())
.productName(product.getProductName())
.price(product.getPrice())
.build();
}
}

TransactionService:

@Service
@RequiredArgsConstructor
public class TransactionService {

private final TransactionRepository transactionRepository;

public List<TransactionDTO> getAllTransactions() {
return transactionRepository.findAll().stream()
.map(this::mapToDTO)
.collect(Collectors.toList());
}

public TransactionDTO createTransaction(Transaction transaction) {
Transaction savedTransaction = transactionRepository.save(transaction);
return mapToDTO(savedTransaction);
}

private TransactionDTO mapToDTO(Transaction transaction) {
return TransactionDTO.builder()
.qrCode(transaction.getCustomer().getQrCode())
.rfId(transaction.getProduct().getRfId())
.price(transaction.getPrice())
.totalPrice(transaction.getTotalPrice())
.date(transaction.getDate())
.build();
}
}

Step 2: Create APIs using ExpressJS (TypeScript), Redis, MongoDB, and GraphQL

In this step, we will build APIs for transactions using ExpressJS, Redis for caching, MongoDB for NoSQL database storage, and GraphQL.

2.1. Set Up ExpressJS with TypeScript

  • Initialize a new Express project with TypeScript.
npm install express pg mongoose ioredis graphql express-graphql apollo-server-express
npm install --save-dev typescript ts-node-dev eslint

2.2. MongoDB Setup

// mongo.config.ts
import mongoose from 'mongoose';

const connectMongo = async () => {
try {
await mongoose.connect(process.env.MONGO_URI as string);
console.log('MongoDB connected');
} catch (error) {
console.error('MongoDB connection error:', error);
}
};
export default connectMongo;


// redis.config.ts
import Redis from 'ioredis';
const redisClient = new Redis(
process.env.REDIS_URL || 'redis://localhost:6379'
);

redisClient.on('error', (err) => {
console.error('Redis Client Error', err);
});

const connectRedis = async () => {
redisClient.on('connect', () => {
console.log('Redis connected successfully');
});
};
export { redisClient, connectRedis };


// postgres.config.ts
import { Pool } from 'pg';

const pool = new Pool({
user: process.env.PG_USER,
host: process.env.PG_HOST,
database: process.env.PG_DATABASE,
password: process.env.PG_PASSWORD,
port: parseInt(process.env.PG_PORT || '5432')
});

const connectPostgres = async () => {
try {
await pool.connect();
console.log('PostgreSQL connected successfully');
} catch (err) {
console.error('PostgreSQL Connection Error:', err);
process.exit(1);
}
};

export { pool, connectPostgres };

2.3 Resolver

A GraphQL resolver is a function or method that is responsible for retrieving the data needed to respond to a GraphQL request.

import ProductService from '../services/product.service';
import TransactionService from '../services/transaction.service';

const resolvers = {
Query: {
getProductByRfId: async (_: any, { rf_id }: { rf_id: string }) => {
return await ProductService.getProductByRfId(rf_id);
}
},
Mutation: {
checkinProduct: async (
_: any,
{
rf_id,
product_name,
price
}: { rf_id: string; product_name: string; price: number }
) => {
await ProductService.checkinProduct({ rf_id, product_name, price });
return 'Product check-in successful';
},
saveTransaction: async (
_: any,
{
qr_code,
rf_id,
price,
total_price
}: { qr_code: string; rf_id: string; price: number; total_price: number }
) => {
await TransactionService.saveTransactionToMongo({
qr_code,
rf_id,
price,
total_price
});
await TransactionService.transferTransactionToPostgres({
qr_code,
rf_id,
price,
total_price
});
return 'Transaction saved and transferred successfully';
}
}
};

export default resolvers;

2.4 Create Service

// Product Service
import { redisClient } from '../configs/redis.config';

interface Product {
rf_id: string;
product_name: string;
price: number;
}

class ProductService {
async checkinProduct(product: Product): Promise<void> {
const { rf_id, product_name, price } = product;

await redisClient.set(rf_id, JSON.stringify({ product_name, price }));
}

async getProductByRfId(rf_id: string): Promise<Product | null> {
const product = await redisClient.get(rf_id);
if (product) {
return JSON.parse(product);
}
return null;
}
}

export default new ProductService();


// Transaction Service
import { v4 as uuidv4 } from 'uuid';
import { pool } from '../configs/postgres.config';
import { Transaction } from '../models/transaction.model';

interface TransactionData {
qr_code: string;
rf_id: string;
price: number;
total_price: number;
}

class TransactionService {
async saveTransactionToMongo(data: TransactionData): Promise<void> {
const transaction = new Transaction(data);
await transaction.save();
}

async transferTransactionToPostgres(data: TransactionData): Promise<void> {
const client = await pool.connect();
const id = uuidv4();
const date = new Date();

try {
await client.query('BEGIN');

const insertTransactionQuery = `
INSERT INTO transactions (id, qr_code, rf_id, price, total_price, date)
VALUES ($1, $2, $3, $4, $5, $6)
`
;
await client.query(insertTransactionQuery, [
id,
data.qr_code,
data.rf_id,
data.price,
data.total_price,
date
]);

const updateCustomerWalletQuery = `
UPDATE customers
SET wallet = wallet - $1
WHERE qr_code = $2
RETURNING wallet
`
;

const updateResult = await client.query(updateCustomerWalletQuery, [
data.total_price,
data.qr_code
]);

if (updateResult.rowCount === 0) {
throw new Error(`Customer with qr_code ${data.qr_code} not found`);
}

await client.query('COMMIT');
console.log('Transaction and wallet update successfully');
} catch (error) {
await client.query('ROLLBACK');
console.error('Error executing transaction and wallet update:', error);
throw error;
} finally {
client.release();
}
}
}

export default new TransactionService();

2.5 Final Setup :

// schema.ts
import { gql } from 'apollo-server-express';

const typeDefs = gql`
type Product {
rf_id: String!
product_name: String!
price: Float!
}

type Transaction {
qr_code: String!
rf_id: String!
price: Float!
total_price: Float!
}

type Query {
getProductByRfId(rf_id: String!): Product
}

type Mutation {
checkinProduct(rf_id: String!, product_name: String!, price: Float!): String
saveTransaction(
qr_code: String!
rf_id: String!
price: Float!
total_price: Float!
): String
}
`;

export default typeDefs;
// app.ts
import express from 'express';
import dotenv from 'dotenv';
import cors from 'cors';
import { ApolloServer } from 'apollo-server-express';
import typeDefs from './schema';
import resolvers from './resolvers';
import connectMongo from './configs/mongo.config';
import { connectRedis } from './configs/redis.config';
import { connectPostgres } from './configs/postgres.config';

dotenv.config();

const app = express();

// Database and Redis connections
connectMongo();
connectRedis();
connectPostgres();

// Middleware
app.use(express.json());
app.use(cors());

async function startApolloServer() {
const server = new ApolloServer({
typeDefs,
resolvers
});

await server.start();
server.applyMiddleware({ app });

return app;
}

export default startApolloServer;
// index.ts
import 'dotenv/config';
import startApolloServer from './app';

const PORT = process.env.PORT || 3000;

startApolloServer().then((app) => {
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
});

Step 3: Frontend with ReactJS, TypeScript and Tailwind CSS

In this step, we will create the frontend layout and integrate it with the backend APIs.

3.1. Set Up React with Vite

  • Initialize a new Vite project with React and TypeScript
npm create vite@latest cashier-app --template react

This will create a new React project with Vite.

3.2: Install and Configure Tailwind CSS

Tailwind CSS is a utility-first CSS framework that makes styling your app fast and efficient.

  1. Install Tailwind CSS:

In your project directory, run the following command:

npm install -D tailwindcss postcss autoprefixer

2. Initialize Tailwind config:

npx tailwindcss init

This will generate a tailwind.config.js file in your project.

3. Configure Tailwind in your project:

Open tailwind.config.js and replace the content with the following:

module.exports = {
content: [
'./index.html',
'./src/**/*.{js,ts,jsx,tsx}',
],
theme: {
extend: {},
},
plugins: [],
};

4. Create Tailwind CSS file:

Create a new file src/index.css and add the following:

@tailwind base;
@tailwind components;
@tailwind utilities;

5. Import Tailwind CSS in main.tsx:

Open src/main.tsx and import the Tailwind CSS file:

import './index.css';

3.3: Setting Up Apollo Client and GraphQL

Now, we will integrate Apollo Client and GraphQL to enable communication with the backend.

  1. Install Apollo Client and GraphQL:

Run the following command to install the required packages:

npm install @apollo/client graphql

2. Set up Apollo Client in Main:

Open src/main.tsx and wrap the App component with ApolloProvider:

import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { ApolloClient, InMemoryCache, ApolloProvider } from "@apollo/client";
import App from "./App.tsx";
import "./index.css";
import { Toaster } from "sonner";

const client = new ApolloClient({
uri: "http://localhost:3000/graphql",
cache: new InMemoryCache(),
});

createRoot(document.getElementById("root")!).render(
<StrictMode>
<ApolloProvider client={client}>
<App />
<Toaster />
</ApolloProvider>
</StrictMode>
);

3.4: Building the QR Code Cashier App

Now that our setup is ready, let’s build the main functionality of the app: scanning a QR code and saving transactions.

  1. Install html5-qrcode:

We will use the html5-qrcode library to scan QR codes.

npm install html5-qrcode

2. Create the Shopping Component:

Now, let’s build the Shopping component where users can scan a QR code and perform a transaction.

Create a new file src/components/Shopping.tsx:

import { useState, useEffect, useRef } from "react";
import { Html5QrcodeScanner } from "html5-qrcode";
import { toast } from "sonner";
import { useNavigate } from "react-router-dom";
import { useMutation, gql } from "@apollo/client";

const SAVE_TRANSACTION = gql`
mutation SaveTransaction(
$qr_code: String!
$rf_id: String!
$price: Float!
$total_price: Float!
) {
saveTransaction(
qr_code: $qr_code
rf_id: $rf_id
price: $price
total_price: $total_price
)
}
`
;

const Shopping = () => {
const [qrData, setQrData] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const qrCodeRegionRef = useRef<HTMLDivElement | null>(null);
const scannerRef = useRef<Html5QrcodeScanner | null>(null);
const navigate = useNavigate();

const [saveTransaction, { loading, error: mutationError }] =
useMutation(SAVE_TRANSACTION);

useEffect(() => {
if (qrCodeRegionRef.current && !scannerRef.current) {
const scanner = new Html5QrcodeScanner(
qrCodeRegionRef.current.id,
{
fps: 10,
qrbox: { width: 250, height: 250 },
},
false
);
scannerRef.current = scanner;

scanner.render(
(decodedText) => {
setQrData(decodedText);
setError(null);
scanner.clear();
scannerRef.current = null;
},
() => {
setError("QR scanning failed. Please try again.");
}
);

return () => {
if (scannerRef.current) {
scannerRef.current.clear();
scannerRef.current = null;
}
};
}
}, []);

const handleCheckout = async () => {
if (!qrData) {
toast.error("Please scan a QR code before proceeding!");
return;
}

try {
const { data } = await saveTransaction({
variables: {
qr_code: "C001",
rf_id: "P001",
price: 1250.0,
total_price: 1250.0,
},
});

toast.success(data.saveTransaction, {
position: "top-center",
});

navigate("/");
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (err) {
toast.error("Failed to complete the transaction. Please try again.");
}
};

return (
<div className="mx-auto mt-8 mb-40">
<div>
<h1 className="font-laonoto mt-4 text-center text-xl font-bold">
Adi Munawar
</h1>
<p className="mt-1 text-center text-xl font-medium text-blue-500">
040-12-00-01166166-001
</p>
</div>

{/* QR Code Scanner */}
<div className="mt-8 text-center">
<h2 className="text-lg font-semibold mb-4">Scan QR for Payment</h2>
<div id="qr-reader" ref={qrCodeRegionRef} />
{qrData ? (
<p className="mt-2 text-green-500">QR Data: {qrData}</p>
) : (
<p className="mt-2 text-gray-500">No QR code detected yet</p>
)}
{error && <p className="text-red-500 mt-2">{error}</p>}
</div>

<button
onClick={handleCheckout}
className="rounded w-full mt-4 px-2 py-3 border border-yellow-400 hover:border-yellow-700 bg-gradient-to-b from-yellow-300 to-yellow-500 hover:from-yellow-400 hover:to-yellow-600"
disabled={loading}
>
{loading ? "Processing..." : "Checkout"}
</button>

{mutationError && (
<p className="text-red-500 mt-2">{mutationError.message}</p>
)}
</div>
);
};

export default Shopping;

This component handles the QR code scanning using html5-qrcode, collects the QR data, and allows the user to perform a transaction using Apollo Client to send the data to the server.

3.3. Integrate APIs

Use Axios or Fetch to call the backend APIs and render data dynamically in the UI components.

Create a new file src/components/TransactionHistory.tsx:

import { useState, useEffect } from "react";
import dayjs from "dayjs";
import apiSpring from "../api/apiSpring";
import { TransactionInterface } from "../interfaces/transactions";

const TransactionHistory = () => {
const [transactions, setTransactions] = useState<TransactionInterface[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");

const fetchTransactions = async () => {
try {
const response = await apiSpring.get("/transactions");
console.log("Response: ", response);

setTransactions(response.data);
setLoading(false);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (err) {
setError("Failed to fetch transactions. Please try again later.");
setLoading(false);
}
};

useEffect(() => {
fetchTransactions();
}, []);

if (loading) {
return <div className="text-center mt-8">Loading...</div>;
}

if (error) {
return <div className="text-center text-red-500 mt-8">{error}</div>;
}

return (
<div className="mx-auto p-4 pb-40">
{/* Header */}
<div className="bg-gray-900 text-white py-2 -mt-4 rounded">
<h1 className="text-center text-md text-gray-200 font-bold">
Transaction History
</h1>
</div>

{/* Transactions List */}
<div className="space-y-4 mt-4">
{transactions.length === 0 ? (
<div className="text-center text-gray-500">
No transactions found.
</div>
) : (
transactions.map((transaction) => (
<div
key={transaction.qrCode}
className="bg-white rounded-lg p-4 shadow-md"
>
<div className="flex justify-between">
<div>
<h3 className="font-semibold">{transaction.qrCode}</h3>
<p>{transaction.rfId}</p>
<p className="text-gray-500">
{dayjs(transaction.date).format("MMM D, YYYY h:mm A")}
</p>
</div>
<div>
<span className="font-bold text-yellow-500">
${transaction.price.toFixed(2)}
</span>
<p className="text-gray-500">
${transaction.totalPrice.toFixed(2)}
</p>
</div>
</div>
</div>
))
)}
</div>
</div>
);
};

export default TransactionHistory;

5.5: Add Routing

Now let’s add React Router to handle routing between pages.

  1. Install React Router DOM:
npm install react-router-dom

2. Configure Routes:

Update your src/App.tsx to set up basic routes:

import { BrowserRouter as Router, Route, Routes } from "react-router-dom";
import BottomNav from "./components/BottomNav";
import HeaderNav from "./components/HeaderNav";
import Home from "./pages/Home";
import Shopping from "./pages/Shopping";
import TransaksiAdd from "./pages/TransaksiAdd";
import TransaksiHistory from "./pages/TransaksiHistory";
import Data from "./pages/Data";
import CustomerList from "./pages/Customers";
import ProductList from "./pages/Products";
import Profile from "./pages/Profile";

export default function App() {
return (
<Router>
<div className="relative flex items-center justify-center">
<div className="absolute bg-black opacity-10 inset-0 z-0" />
<div className="w-[400px] min-h-screen overflow-y-scroll space-y-3 bg-white z-10">
<HeaderNav />
<div className="px-4">
<Routes>
<Route path="/" element={<Home />} />
<Route path="/shopping" element={<Shopping />} />
<Route path="/transaksi-add" element={<TransaksiAdd />} />
<Route path="/transaksi-history" element={<TransaksiHistory />} />
<Route path="/data" element={<Data />} />
<Route path="/data/customers" element={<CustomerList />} />
<Route path="/data/products" element={<ProductList />} />
<Route path="/profile" element={<Profile />} />
</Routes>
</div>
<BottomNav />
</div>
</div>
</Router>
);
}

6.6: Run Your Application

Now that everything is set up, run the application:

npm run dev

In this section, we covered how to set up a complete project with Vite, React, Tailwind CSS, and Apollo GraphQL. We also built a QR code shopping app where users can scan a QR code and perform a transaction based on the scanned information.

To wrap things up, I hope this tutorial has provided you with valuable insights into building a mini cashier application using a combination of modern technologies such as Spring Boot, ExpressJS, Redis, GraphQL, and ReactJS. From backend setup to frontend integration, this guide serves as a foundation for creating even more complex systems in the future.

For further clarification or if you’d like to explore the source code in more detail, feel free to check out the following GitHub repositories:

  1. Spring Boot API: Spring Boot GitHub Repo
  2. ExpressJS API with Redis and MongoDB: ExpressJS GitHub Repo
  3. ReactJS Frontend: ReactJS GitHub Repo

Each repository contains detailed comments and documentation to help you better understand the implementation.

Happy coding, and I hope you enjoy building your own projects!

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

D2Y MVN
D2Y MVN

Written by D2Y MVN

Lets make a plane and take a risk!

No responses yet

Write a response

Recommended from Medium

Lists

See more recommendations