Every banking dashboard project starts with the same three problems: you need a frontend to render data, an admin interface to manage that data, and an API to connect the two. Most teams either build a custom admin panel from scratch, burning days on CRUD forms, or hardcode mock JSON and never get around to making the data editable.
Strapi removes the admin UI build entirely. You define your data models, and Strapi generates both the admin panel and the REST API automatically. That leaves you free to focus on the frontend layout, data visualization, and user experience.
By the end of this tutorial, you'll have a working banking dashboard with three content types (Account, Transaction, Card), a transactions list sorted by date, and a category-spending bar chart, all powered by live data from Strapi's REST API. The Next.js frontend fetches everything server-side and hydrates interactive components on the client.
One caveat up front: this is for prototyping and learning. Real banking infrastructure requires encryption, regulatory compliance, and authentication layers that go well beyond what a tutorial can responsibly cover.
In brief:
- You're building a banking-style dashboard with account balances, card details, a transactions table, and a spending-by-category chart.
- The stack is Strapi 5, a headless CMS, and Next.js 16 with the App Router, using Recharts for data visualization.
- Expect about 45 minutes from the first command to a working prototype. Basic React and Next.js familiarity is assumed.
- This targets a demo or prototyping use case, not a production fintech system. All code is referenced inline throughout the tutorial.
What You'll Build
The finished banking dashboard with Strapi and Next.js includes data visualization components for reporting and analysis.
The architecture is straightforward: Strapi 5 serves data via its REST API, and the Next.js App Router renders the UI server-side. Interactive pieces like the chart use client components that receive pre-fetched data as props. Strapi acts as the headless CMS, providing both the admin panel for content editors and the API layer that the frontend consumes.
Tech Stack and Versions
- Strapi 5 (
npx create-strapi@latest) - Next.js 16 (App Router)
- Node.js v20, v22, or v24 (supported by Strapi 5; only Active LTS or Maintenance LTS versions are supported)
- SQLite (default Strapi database; no external DB needed for this tutorial)
- Recharts for the chart component
Strapi pairs well with modern frontend frameworks. The Strapi integrations page and related Strapi resources list connectors and integrations for Next.js, Nuxt, and others.
Prerequisites
Before starting, confirm you have:
- Node.js LTS (v20, v22, or v24) installed. Odd-numbered releases like v23 or v25 are not supported by Strapi 5.
- Basic familiarity with React and Next.js, including components, props, and
async/await. - Terminal access for running CLI commands.
Prior Strapi experience is helpful but not required. The tutorial walks through every step.
Sign up for the Logbook, Strapi's Monthly newsletter
Set Up the Strapi Backend
Strapi 5 scaffolds quickly with one CLI command. Two steps get the backend running.
Create the Strapi 5 Project
Open your terminal and run:
npx create-strapi@latest banking-dashboard-apiThe interactive installer will walk you through several prompts:
- Strapi Cloud login/signup: You can skip this or log in. Use the
--skip-cloudflag if you prefer to bypass it. - TypeScript or JavaScript: Pick whichever you're comfortable with. TypeScript is the default.
- Install dependencies: Accept to auto-install.
- Initialize a git repository: Your call, but "yes" is a safe default.
- Use default SQLite database: Select yes. SQLite requires no external database setup, which keeps this tutorial focused.
Note that the --quickstart flag is deprecated in Strapi 5. The --non-interactive flag replaces it. For a fully automated setup with no prompts, use the --non-interactive flag instead, which accepts all defaults (TypeScript, SQLite, install deps, init git).
Start the Development Server
Navigate into the project and start Strapi:
cd banking-dashboard-api && npm run developThe Admin Panel opens at http://localhost:1337/admin. On the first launch, you'll see a registration form. Fill in your name, email, and password to create the admin user. This is your local admin account, not a Strapi Cloud account.
If Strapi fails to start or throws unexpected errors during installation, confirm your Node.js version is v20, v22, or v24. Odd-numbered releases (v21, v23, v25) are not supported. Run node -v to check. The install docs list the full runtime requirements.
Model the Banking Data in Strapi
The banking dashboard needs three collection types and two relations. Each type maps to one section of the frontend UI.
Create the Account Collection Type
In the Admin Panel, navigate to Content-Type Builder and click Create new collection type. Name it Account.
Add these fields:
accountName: Text (Short text)accountNumber: Text (Short text). In production, this would be encrypted. For the tutorial, plain text is fine.accountType: Enumeration with valuesChecking,Savings,Credit(one per line in the values editor)balance: Number (decimal)currency: Text (Short text), with a default value ofUSD
Click Save. Strapi writes the schema to disk; depending on how the app is running, you may need to restart the server for the changes to take effect. Wait for the restart to finish before proceeding.
Create the Transaction Collection Type
Same flow. Create a new collection type named Transaction with these fields:
description: Text (Short text)amount: Number (decimal)transactionDate: Date (datetime type)category: Enumeration with valuesGroceries,Utilities,Dining,Transport,Income,OthertransactionType: Enumeration with valuesdebit,credit
Save and wait for the restart.
Create the Card Collection Type
Create one more collection type named Card:
cardholderName: Text (Short text)last4: Text (Short text, max length 4)cardType: Enumeration with valuesVisa,Mastercard,AmexexpiryDate: Date (date type, year/month/day)
Save again.
Define Relations Between Content Types
Relations connect these Strapi content types so transactions and cards belong to specific accounts.
Transaction → Account relation:
- Open the
Transactioncollection type in the Content-Type Builder. - Add a new Relation field.
- In the relation configuration window, select
Accountas the target content type (the second grey box). - Click the Many-to-One icon. This means many transactions belong to one account.
- Name the field
account.
Card → Account relation:
- Open the
Cardcollection type. - Add a Relation field targeting
Account. - Select Many-to-One again. Many cards can belong to one account.
- Name the field
account.
Save. Both relations will render as dropdown selectors in the Content Manager when creating entries.
One thing to note: relations require the populate parameter when fetching via REST. Without the populate parameter in the fetch URL, Strapi only returns default attributes and does not return any relation fields at all. Double-check your fetch calls when building the frontend so each request that needs related data includes this parameter. More on that when you build the frontend.
For deeper background on structuring data models, the content modeling guide covers the theory behind these decisions.
Configure API Access and Seed Sample Data
Strapi locks down all content types by default. You need to open read permissions and populate the database before the frontend can fetch anything.
Grant Public Read Permissions
By default, new Strapi content types are not publicly accessible to the Public role unless permissions are granted. The Public role needs explicit permissions before the Next.js app can fetch data.
Navigate to Settings → Users & Permissions plugin → Roles → Public.
You'll see a permissions table listing each collection type. For Account, Transaction, and Card, enable the find and findOne checkboxes. Click Save.
This opens unauthenticated GET access to your API endpoints. For a real application, you'd replace public access with API tokens or authenticated user sessions. For prototyping, public read access is sufficient.
Add Sample Data via the Content Manager
Go to Content Manager and create some entries:
Accounts (create two):
- "Main Checking" / Checking / balance: 4250.75 / USD
- "Savings Reserve" / Savings / balance: 12800.00 / USD
Transactions (create five to ten across both accounts):
- Vary the categories (Groceries, Dining, Utilities, Transport, Income), amounts, dates, and transaction types (debit/credit). Assign each transaction to one of the two accounts using the relation dropdown.
Cards (create two):
- Link each card to an account. Use realistic last-4 digits (e.g., "4829"), a cardholder name, card type, and expiry date.
After saving each entry, you must Publish it if Draft & Publish is enabled for that content type. Strapi 5 supports Draft & Publish, and you can publish content by clicking the Publish button in the Entry box on the right side of the edit view.
By default, the Document Service API returns the draft version of a document when Draft & Publish is enabled, and you can use the status parameter (for example, status: 'published') if you want only published entries in responses. If you created entries but see an empty data array when testing the API, go back to the Content Manager and check for the blue "Published" status badge on each entry. Any entry still in "Draft" status will not be returned.
Before moving to the frontend, verify your API is working. Open http://localhost:1337/api/accounts in a browser and confirm you see a JSON response with your account data in the data array.
You can also test http://localhost:1337/api/transactions?populate=account to verify that relations are populated correctly. If the account field shows data for each transaction, your permissions and content are configured correctly. This step saves debugging time later.
Set Up the Next.js Frontend
Backend done. Time to consume the API.
Create the Next.js Project
npx create-next-app@latest banking-dashboard-uiWhen prompted, accept the Next.js defaults: TypeScript, ESLint, Tailwind CSS, and App Router. Then install Recharts:
cd banking-dashboard-ui
npm install rechartsConfigure Environment Variables
Create a .env.local file in the project root:
NEXT_PUBLIC_STRAPI_URL=http://localhost:1337The NEXT_PUBLIC_ prefix is required for client-side access in Next.js. Values with this prefix get inlined into the JavaScript bundle at build time, so use it only for safe, public URLs.
Build a Strapi Fetch Helper
Create lib/strapi.ts to centralize API calls:
const BASE_URL = process.env.NEXT_PUBLIC_STRAPI_URL ?? "";
export async function fetchAPI<T>(
path: string,
params?: Record<string, string>
): Promise<T> {
const url = new URL(`/api${path}`, BASE_URL);
if (params) {
Object.entries(params).forEach(([key, value]) => {
url.searchParams.set(key, value);
});
}
const res = await fetch(url.toString());
if (!res.ok) {
throw new Error(`API error: ${res.status} ${res.statusText}`);
}
return res.json() as Promise<T>;
}Keeping fetch logic in one file makes auth and deployment changes easier later. The generic type parameter <T> lets each call site define the expected response shape, which improves type safety across the project. If you add an authorization header later, this is the only file you need to update.
A quick note on the Strapi 5 response shape: responses usually return an object with data and, optionally, meta keys. If you're coming from Strapi v4, the big change is that attributes are now directly on the data object.
There's no more data.attributes wrapper. So data.title works directly instead of data.attributes.title. The migration guide covers the structural differences in detail. For the full parameter reference (filtering, pagination, field selection), the REST API docs are the authoritative source.
Build the Dashboard UI
The dashboard is a server component that fetches all data in parallel, then passes slices of it to presentational components. Client components handle the interactive pieces.
Create the Dashboard Layout
In app/page.tsx, fetch accounts, transactions, and cards simultaneously using Promise.all:
import { fetchAPI } from "@/lib/strapi";
import AccountCards from "@/components/AccountCards";
import TransactionList from "@/components/TransactionList";
import CardSection from "@/components/CardSection";
import SpendingChart from "@/components/SpendingChart";
interface StrapiResponse<T> {
data: T[];
meta: object;
}
export default async function DashboardPage() {
const [accountsRes, transactionsRes, cardsRes] = await Promise.all([
fetchAPI<StrapiResponse<any>>("/accounts"),
fetchAPI<StrapiResponse<any>>("/transactions", {
populate: "account",
"sort": "transactionDate:desc",
}),
fetchAPI<StrapiResponse<any>>("/cards", {
populate: "account",
}),
]);
const accounts = accountsRes.data;
const transactions = transactionsRes.data;
const cards = cardsRes.data;
// Aggregate spending by category for the chart
const spendingByCategory = transactions
.filter((tx: any) => tx.transactionType === "debit")
.reduce((acc: Record<string, number>, tx: any) => {
const cat = tx.category || "Other";
acc[cat] = (acc[cat] || 0) + tx.amount;
return acc;
}, {});
const chartData = Object.entries(spendingByCategory).map(
([category, amount]) => ({ category, amount })
);
return (
<main className="min-h-screen bg-gray-50 p-8">
<h1 className="text-3xl font-bold mb-8">Banking Dashboard</h1>
<AccountCards accounts={accounts} />
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 mt-8">
<TransactionList transactions={transactions} />
<div className="space-y-8">
<CardSection cards={cards} />
<SpendingChart data={chartData} />
</div>
</div>
</main>
);
}The populate parameter is the key detail here. Without populate=account, the account relation on each transaction is not populated and is typically not included in the response. The populate docs cover more advanced nesting scenarios.
The two-column grid on large screens puts the transaction list, the most data-dense element, in the left column, while cards and the spending chart stack vertically in the right column. On mobile, everything stacks into a single column. This pattern works well for dashboards because the transaction table needs horizontal space, while the card and chart components are more compact and work well stacked vertically.
Render Account Balance Cards
Create components/AccountCards.tsx:
export default function AccountCards({ accounts }: { accounts: any[] }) {
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{accounts.map((account) => (
<div
key={account.documentId}
className="bg-white rounded-xl p-6 shadow-sm"
>
<p className="text-sm text-gray-500">{account.accountType}</p>
<h2 className="text-lg font-semibold mt-1">{account.accountName}</h2>
<p className="text-2xl font-bold mt-3">
{new Intl.NumberFormat("en-US", {
style: "currency",
currency: account.currency || "USD",
}).format(account.balance)}
</p>
</div>
))}
</div>
);
}The balance and currency values come straight from the Strapi response with no transformation needed. Intl.NumberFormat handles the locale-specific formatting.
Display the Transactions List
Next, add components/TransactionList.tsx for the transactions table:
export default function TransactionList({
transactions,
}: {
transactions: any[];
}) {
return (
<div className="bg-white rounded-xl p-6 shadow-sm">
<h2 className="text-xl font-semibold mb-4">Recent Transactions</h2>
<table className="w-full text-sm">
<thead>
<tr className="text-left text-gray-500 border-b">
<th className="pb-2">Date</th>
<th className="pb-2">Description</th>
<th className="pb-2">Category</th>
<th className="pb-2 text-right">Amount</th>
</tr>
</thead>
<tbody>
{transactions.map((tx) => (
<tr key={tx.documentId} className="border-b last:border-0">
<td className="py-3">
{new Date(tx.transactionDate).toLocaleDateString()}
</td>
<td className="py-3">{tx.description}</td>
<td className="py-3">
<span className="bg-gray-100 text-gray-700 px-2 py-1 rounded text-xs">
{tx.category}
</span>
</td>
<td
className={`py-3 text-right font-medium ${
tx.transactionType === "credit"
? "text-green-600"
: "text-red-600"
}`}
>
{tx.transactionType === "credit" ? "+" : "-"}$
{tx.amount.toFixed(2)}
</td>
</tr>
))}
</tbody>
</table>
</div>
);
}The query string sort=transactionDate:desc in the fetch call handles ordering. For more complex filtering, such as date ranges or multiple conditions, the qs library is the standard way to build Strapi query strings. You can install it with npm install qs and construct filters programmatically.
Add the Cards Section
The card display goes in components/CardSection.tsx:
export default function CardSection({ cards }: { cards: any[] }) {
const formatExpiry = (dateStr: string) => {
const date = new Date(dateStr);
const month = String(date.getMonth() + 1).padStart(2, "0");
const year = String(date.getFullYear()).slice(-2);
return `${month}/${year}`;
};
return (
<div className="bg-white rounded-xl p-6 shadow-sm">
<h2 className="text-xl font-semibold mb-4">Your Cards</h2>
<div className="space-y-4">
{cards.map((card) => (
<div
key={card.documentId}
className="bg-gradient-to-r from-slate-800 to-slate-600 text-white rounded-lg p-5"
>
<p className="text-xs tracking-wider opacity-75">
{card.cardType}
</p>
<p className="text-lg tracking-widest mt-4 font-mono">
•••• •••• •••• {card.last4}
</p>
<div className="flex justify-between mt-4 text-sm">
<span>{card.cardholderName}</span>
<span>{formatExpiry(card.expiryDate)}</span>
</div>
</div>
))}
</div>
</div>
);
}For additional ideas on styling components with Tailwind animations guide in a Strapi and Next.js project, the animations tutorial covers some useful patterns.
Visualize Spending with a Category Chart
The spending chart is the one client component in the dashboard. The server component handles data aggregation, and the client component handles rendering with Recharts.
Aggregate Transactions by Category
The reducer in the dashboard page groups transactions by category and sums the amount for debits only:
const spendingByCategory = transactions
.filter((tx: any) => tx.transactionType === "debit")
.reduce((acc: Record<string, number>, tx: any) => {
const cat = tx.category || "Other";
acc[cat] = (acc[cat] || 0) + tx.amount;
return acc;
}, {});
const chartData = Object.entries(spendingByCategory).map(
([category, amount]) => ({ category, amount })
);This runs on the server before the page renders. The chart component receives a clean array of { category, amount } objects as props, which keeps the heavier data work off the client.
Render the Chart Component
Add components/SpendingChart.tsx as a client component:
"use client";
import {
BarChart,
Bar,
XAxis,
YAxis,
Tooltip,
ResponsiveContainer,
} from "recharts";
interface SpendingData {
category: string;
amount: number;
}
export default function SpendingChart({ data }: { data: SpendingData[] }) {
return (
<div className="bg-white rounded-xl p-6 shadow-sm">
<h2 className="text-xl font-semibold mb-4">Spending by Category</h2>
<div aria-label="Bar chart showing spending breakdown by category" className="w-full h-[300px]">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={data}>
<XAxis dataKey="category" />
<YAxis unit="$" />
<Tooltip />
<Bar dataKey="amount" fill="#4845fe" radius={[8, 8, 0, 0]} />
</BarChart>
</ResponsiveContainer>
</div>
</div>
);
}The use client at the top is typically needed in Next.js because Recharts charts are rendered as interactive client components. Without it, you may run into server-side rendering or hydration issues. The parent div with an explicit height (300px) avoids the common ResponsiveContainer gotcha where charts render at 0px when the parent has no defined height.
The aria-label on the chart wrapper gives screen readers context for the category breakdown visualization.
The fill="#4845fe" prop on the Bar component controls the bar color and can be changed to match any brand palette. For more advanced use cases, Recharts supports custom tooltip formatters to show dollar signs and decimals, click handlers on individual bars to drill into a specific category, and multiple Bar components for comparing periods side by side. You can also add a CartesianGrid component with strokeDasharray="3 3" for background reference lines that improve readability on dense datasets. Plugins from the Strapi Marketplace can extend Strapi's functionality further if you need additional data processing or custom field types for your charts.
Run the Dashboard and Next Steps
Start both projects in separate terminal windows:
Terminal 1 (Strapi):
cd banking-dashboard-api && npm run developTerminal 2 (Next.js):
npm run devOpen http://localhost:3000 to see the dashboard pulling live data from Strapi. Add or edit entries in the Strapi Admin Panel at http://localhost:1337/admin, and the dashboard reflects changes on the next page load.
Three directions to take this further:
- Replace public permissions with an API token. Add an
Authorization: Bearer <token>header to your fetch helper and disable public access on the Strapi roles. - Add user authentication via Strapi's
/api/auth/localendpoint so each user sees only their own accounts, filtered by their identity. - Deploy the stack: backend to Strapi Cloud or your own infrastructure, frontend to Vercel. Update
NEXT_PUBLIC_API_URLto point to the production Strapi instance.
The prototype you've built here gives you a working data layer, an admin interface, and a rendered frontend, which is the foundation for any headless CMS architecture financial dashboard project.
Get Started in Minutes
npx create-strapi-app@latest in your terminal and follow our Quick Start Guide to build your first Strapi project.