The incredible thing about Blitz is that it comes with the basic stuff we will need when building a web app. Blitz.js generates for you a default client-side implementation that you customize along with backend code to support it. Let's see it in action!
In this post, we will build a banking admin app using the Blitz.js framework. This tutorial will help us understand the core concepts of the Blitz.js framework and learn how we can apply the concepts to build real-world applications.
The banking admin app will be able to:
Create accounts
View transactions in an account
Make credits
Make debits
Read on to see how all these can be done in a Blitz.js app.
Blitz.js
Blitz.js is a full-stack React framework built on Nextjs, it was inspired by Ruby on Rails.
The incredible thing about Blitz is that it comes with the basic stuff we will need when building a web app. Blitz.js generates for you a default client-side implementation that you customize along with backend code to support it.
Blitz.js uses Prisma ORM client for database connection, object modeling, and migrations.
API endpoints in Blitz.js are written in mutation and query resolvers. These resolvers are functions called from components, they are where data are added or retrieve from the database using the Prisma client.
With Blitz, all we have to do is scaffold the project and we are almost half-done.
Requirements
We need to have the following tools installed in our machine:
Node.js - Blitz.js is based on Node.js, so we will need to have Node.js installed in our machine. Go to https://nodejs.org/download page to download the binary for your machine.
NPM This is Node packager manager for the Node.js, it comes bundled with Node.js.
Yarn This is another Node package manager, it is faster than NPM. If you decide to use it, install it by running the command npm i -g yarn.
blitz This is the Blitz.js CLI tool, we will need it to scaffold Blitz.js projects, generate files and folders, run Blitz.js servers, and lots more. To install the Blitz.js tool run the command yarn add global blitz.
Building the bank admin app
First, we have to scaffold the Blitz.js project. Run the below command to do that:
blitz new bank-admin
The new sub-command tells Blitz that we want to create a new Blitz.js project. The bank-admin is the name of the folder where the project will reside. The Blitz tool will create the folder and create the files and folder of the project inside it.
The above command at some point would request for a Form library to choose, just hit Enter to choose the recommended one.
✔ Pick a form library (you can switch to something else later if you want) · React Final Form
After that it will generate the files:
Then it will install the dependencies:
and exit.
Move into the bank-admin folder: cd bank-admin.
We are set. Let's look at the routes our app will have.
Our bank admin app will have the below routes:
Routes:
/accounts: List all the accounts in the system. /accounts/:id: View an account. /accounts/new: This page is where new accounts are added to the system.
/transactions: This page is where all transactions carried out are listed. /transactions/:id: This page displays a specific transaction.
/transactions/new: This page is where a transaction is carried out.
Let's create the accounts pages:
blitz g all account
This command creates the files:
app/pages/accounts/[accountId].tsx
app/pages/accounts/[accountId]/edit.tsx
app/pages/accounts/index.tsx
app/pages/accounts/new.tsx
app/accounts/components/AccountForm.tsx
app/accounts/queries/getAccount.ts
app/accounts/queries/getAccounts.ts
app/accounts/mutations/createAccount.ts
app/accounts/mutations/deleteAccount.ts
app/accounts/mutations/updateAccount.ts
The command will create an Account model in the schema.prisma. The command will run migrations on the newly added model and it will prompt for a name for the migration, type added_account and hit Enter.
Let's go through the files:
app/pages/accounts/[accountId].tsx: This file will display the details of an account based on its accountId id.
app/pages/accounts/[accountId]/edit.tsx: This file is where we edit an account.
app/pages/accounts/index.tsx: This file is where all accounts in the system are displayed.
app/pages/accounts/new.tsx
app/accounts/components/AccountForm.tsx: We will remove this, we do not need it.
app/accounts/queries/getAccount.ts: This is a query resolver file. This file gets an account from the database.
app/accounts/queries/getAccounts.ts: This file is a query resolver file too, it returns all the accounts from the database.
app/accounts/mutations/createAccount.ts: This file is a mutation resolver, it is where new accounts are created in the database.
app/accounts/mutations/deleteAccount.ts: This is a mutation resolver, it is here that accounts are deleted from the database.
app/accounts/mutations/updateAccount.ts: This is a mutation resolver, it is here accounts are edited in the database.
Now Let's create the transaction model. Run the below command:
blitz g all transaction
This will create pages, models, mutations, and queries for the transaction model.
Let's go through the files:
app/pages/transactions/[transactionId].tsx: This is where a transaction in the system is displayed.
app/pages/transactions/[transactionId]/edit.tsx: We will not need this file, so we will remove. It should have been where we edit a transaction but in a banking system transactions are not edited so we do away with this.
app/pages/transactions/index.tsx: This file displays the list of transactions that has been carried out in our system.
app/pages/transactions/new.tsx: This file is where a new transaction is carried out.
app/transactions/components/TransactionForm.tsx: We don't need this, so we will remove it.
app/transactions/queries/getTransaction.ts: This is a query resolver that returns a transaction from the database.
app/transactions/queries/getTransactions.ts: This is a query resolver file that returns all the transactions in the database.
app/transactions/mutations/createTransaction.ts: This is a mutation resolver that creates a new transaction in the database.
app/transactions/mutations/deleteTransaction.ts: This is a mutation resolver file, it deletes a transaction from the database. We won't need it because we can't delete transactions from the system.
app/transactions/mutations/updateTransaction.ts: This is a mutation resolver file that modifies a transaction in the database. We won't need this file too because we can modify a transaction.
We will need some components:
Header: This will hold the header section of our app. It will display the name of our app on every page.
AccountCard: This component will render account details on the /accounts page.
TransactionCard: This component will display transaction details on the /transactions page.
We have listed and narrated each component, before we flesh them out let's redefine our Schema models. We have Account and Transaction models in the schema.prisma file. We will need to add more fields to the models:
Account
model Account {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
name String
balance Int @default(0)
}
We added name and balance fields. The name holds the name of the account holder, e.g "John Smith", "Nnamdi Chidume". The balance is the amount of money in dollars the account holder has in the account e.g 10,000, 2,000,000.
Transaction
model Transaction {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
sender Int
receiver Int
amount Int
}
We added the fields: sender, receiver, and amount. The sender is the account sending the money, the receiver is the beneficiary, and the amount is the amount in dollars being sent.
Now we run the migrations:
blitz prisma migrate dev
This command will run migrations on the new fields we added to the models in our schema.prisma.
Let's start coding our components:
Header
Create a folder Header in app/core/components. Create the files: index.tsx and Header.module.css
This component is a presentational component that renders an account object passed to it.
This component will reside in the app/accounts/components folder. So create an AccountCard folder in app/accounts/components and create the files in it: index.tsx and AccountCard.module.css.
See that we destructured account from the props. Also from the account, we destructured the properties from its model: id, name, balance, and createdAt. These fields are what we want the component to display. The UI does the job of displaying the fields.
Paste the styling code in the AccountCard.module.css:
This is the same as the AccountCard component above, it displays the details of a transaction. Create a TransactionCard folder in app/transactions/components, and inside the TransactionCard create index.tsx and TransactionCard.module.css.
Now we have completed the work on our presentational components. We can then proceed working on our Accounts queries and mutations. Blitz scaffolds code with authorization as default and pagination, we don't want them so let’s remove them. Open blitzjs/bank-admin/app/accounts/queries/getAccount.ts
import { resolver, NotFoundError } from "blitz";
import db from "db";
import { z } from "zod";
const GetAccount = z.object({
// This accepts type of undefined, but is required at runtime
id: z.number().optional().refine(Boolean, "Required"),
});
export default resolver.pipe(resolver.zod(GetAccount), async ({ id }) => {
// TODO: in multi-tenant app, you must add validation to ensure correct tenant
const account = await db.account.findFirst({ where: { id } });
if (!account) throw new NotFoundError();
return account;
});
The GetAccount is a schema that defines the inputs of the query resolver. The function retrieves the account by calling db.account.findFirst({ where: { id } }).
The where condition is used to retrieve a specific account whose id is the same as the id we want. The result of the query is then returned.
There was a resolver.authorize() function call passed in as the second arg but we removed it because we don't want authorization. We will do the same with all queries and mutations in this project.
Let's rewrite the blitzjs/bank-admin/app/accounts/queries/getAccounts.ts:
import { NotFoundError, resolver } from "blitz";
import db, { Prisma } from "db";
export default resolver.pipe(async () => {
// TODO: in multi-tenant app, you must add validation to ensure correct tenant
return db.account.findMany();
});
This returns all accounts in the db, the .findMany() call does that.
app/accounts/mutations/createAccount.ts
import { resolver } from "blitz";
import db from "db";
import { z } from "zod";
const CreateAccount = z.object({
name: z.string(),
balance: z.number(),
});
export default resolver.pipe(resolver.zod(CreateAccount), async (input) => {
// TODO: in multi-tenant app, you must add validation to ensure correct tenant
const account = await db.account.create({ data: input });
return account;
});
This creates a new entry in the account table. The db.account.create({ data: input }) does that, the details of the new account is held in the input param, and we see that it is passed to the db.account.create({ data: input }) call.
app/accounts/mutations/deleteAccount.ts
import { resolver } from "blitz";
import db from "db";
import { z } from "zod";
const DeleteAccount = z.object({
id: z.number(),
});
export default resolver.pipe(resolver.zod(DeleteAccount), async ({ id }) => {
// TODO: in multi-tenant app, you must add validation to ensure correct tenant
const account = await db.account.deleteMany({ where: { id } });
return account;
});
This code deletes an account from the database. It does it by calling the db.account.deleteMany({ where: { id } }) method. The where condition is passed to the deleteMany() to tell the database the account by its id to delete.
app/accounts/mutations/updateAccount.ts
import { resolver } from "blitz";
import db from "db";
import { z } from "zod";
const UpdateAccount = z.object({
id: z.number(),
name: z.string(),
});
export default resolver.pipe(
resolver.zod(UpdateAccount),
async ({ id, ...data }) => {
// TODO: in multi-tenant app, you must add validation to ensure correct tenant
const account = await db.account.update({ where: { id }, data });
return account;
}
);
This code updates an already existing account on the database. The db.account.update({ where: { id }, data }) method call did the trick. The where condition tells the database the account we want to update, and the data is the new details of the account.
Now, we are done with the account queries and mutations. Let's move over to the accounts pages.
app/pages/accounts/index.tsx
Blitz has its code already set up for us, but we want a custom UI. This page will fetch all accounts from the database and display them:
Blitz exports useQuery and useMutation hooks. These are used to call our query and mutation resolver respectively. We used the useQuery hook to call the getAccounts query resolver. This returns all the accounts in the database, we stored this result in the accounts variable and use it to display the accounts. See that we used the .map method to render arrays in React, the AccountCard is passed each account to render each account.
app/pages/accounts/new.tsx
Paste the below code to app/pages/accounts/new.tsx:
This code renders input boxes where the name of the account holder and its initial balance are typed in. The “Add Account” button when clicked calls the addAccount function. This function retrieves the values from the input boxes and calls the createAccountMutation function with the same values. The createAccountMutation is the function returned from calling the useMutation hook with createAccount. It calls the resolver function in the createAccount.ts file.
This component retrieves the value of the accountId param from the dynamic route URL using the useParam hook. The account id is passed as param to the useQuery(getAccount, { id: accountId }) method call. This will call the resolver function at getAccount.ts with the account id, this returns the account and sets it to the account variable. This account is used to display the account details on the UI. The transactions done by the account is retrieved too. This is done by calling:
We are telling the database to get us the transactions in which the sender is the account id or the receiver is the account id. In English, it is to return us the transactions of the account id where the id has sent money to another person or received money from another person.
The deleteAccountFn is called by the Delete button. The function calls the deleteAccountMutation function to remove the account from the database.
The details of the account and its transactions are displayed on the UI. The transactions array is rendered in the UI using the .map method, each transaction UI is handled by the TransactionCard component.
app/pages/accounts/[accountId]/edit.tsx
This page will have input boxes filled with the details of an account. The details can be modified and persist in the database.
The id of the account accountId is gotten from the URL using the useParam hook. The getAccount query is called via the useQuery hook, it returns the details of the account id. The values of the input boxes are filled with the details of the account object.
We can then edit the values in the input boxes. Clicking the “Save Account” button runs the editAccount function, the function gathers the values from the input boxes and calls the updateAccountMutation method passing the gathered values to it as params. The id of the account is passed also, this enables the database to ascertain the account to edit. The updateAccountMutation method calls the function in updateAccount.ts to modify the account with the details sent to it. We are done with the Account model, let's move over to Transaction.
Let's start with the mutations. First, we look at createTransaction.
app/transactions/mutations/createTransaction.ts
This file is where we carry out the core function. The credit and debit are made here. Let's see the code:
The input will expect the sender's and receiver's id and the amount.
First, we retrieved the sender account details using the sender id. Then, we subtracted the amount from the balance in the sender's account.
Next, we retrieved the receiver account details and added the amount to the receiver's balance. Finally, we added the transaction to the transaction table.
That is it. When transferring money from one account to another account. The amount to send is debited from the sender's balance, and the amount is added to the beneficiary's balance.
So we have completed it, let's look at the queries. Let's go to getTransaction.
app/transactions/queries/getTransaction.ts
This query simply retrieves a transaction using its id and returns it.
import { resolver, NotFoundError } from "blitz";
import db from "db";
import { z } from "zod";
const GetTransaction = z.object({
// This accepts type of undefined, but is required at runtime
id: z.number().optional().refine(Boolean, "Required"),
});
export default resolver.pipe(resolver.zod(GetTransaction), async ({ id }) => {
// TODO: in multi-tenant app, you must add validation to ensure correct tenant
const transaction = await db.transaction.findFirst({ where: { id } });
if (!transaction) throw new NotFoundError();
return transaction;
});
It uses the where condition arg in the db.transaction.findFirst({ where: { id } }) call to get the specific transaction with the id and return the transaction object.
app/transactions/queries/getTransactions.ts
import { resolver } from "blitz";
import db from "db";
export default resolver.pipe(async ({ where = undefined }) => {
// TODO: in multi-tenant app, you must add validation to ensure correct tenant
return await db.transaction.findMany({ where });
});
This simply returns all the transactions in the database. We passed in where condition for some cases where we might return some transactions. For example, in our accounts/[accountId] page we need to fetch transactions that the account holder has made. So the where helps us, in this case, to fetch the transactions without creating a separate query for it.
app/pages/transactions/index.tsx
This page fetches all transactions from the database and displays them:
It uses the useQuery hook to call the getTransactions query. The result is stored in transactions variable. Since the sender and receiver fields in a transaction just holds ids to the account object, we map through the transactions array and populate them with their account objects.
Then, we render the transactions using the .map method; each transaction is rendered by the TransactionCard component.
app/pages/transactions/[transactionId].tsx
This page fetches the transaction object from the transaction id.
It uses the useParam hook to extract the value of the transactionId from the URL. This id is passed to useQuery(getTransaction, { id: transactionId }) to get the transaction details. Next, we also fetched the sender and receiver account details from the sender and receiver fields in the transaction object. Finally, the transaction details are rendered in the UI.
It has input boxes where we enter the ids of the sender account and the receiver account. There is also an input box where the amount to send is typed. On clicking the “Transact” button, the sender's and receiver's ids are retrieved from the input boxes. Then, the createTransactionMutation method is called with the retrieved values. This call proceeds to make the transaction from both parties and eventually adds the transaction to the transaction table. The fetchAccountReceiver and fetchAccountSender functions retrieve the details of the sender and the receiver respectively from the inputs. This enables the user to see the account name of the sender and the receiver.
We are done building our app. If the Blitz server is not running, spin it up:
yarn dev
# OR
npm run dev
Go to localhost:3000/accounts and start banking.
Test the app
All Accounts
All Transactions
Create Account
Transfer Money
View Account
View Transaction
Delete Account
Conclusion
Blitz.js is an awesome framework, it makes the job easier for devs. We learned a lot here. We started with introducing Blitz.js, detailing its great features and also how to install it. Next, we demonstrated how to use the various features of Blitzjs by creating a minimal banking admin app just like Finacle. Yes, the bank app we built here lacks so many features. I urge you to keep going from this point, it's a great way of learning a new tool and picking up on it very fast.