Real-world code, formulas, and visualization examples
An amortization schedule is the backbone of almost every lending product you’ve ever used: mortgages, car loans, personal loans, even many BNPL plans. It turns a single monthly payment into a month-by-month breakdown of interest paid, principal paid, and remaining balance. For developers building fintech tools, it’s a practical way to learn time-value-of-money math without getting stuck in spreadsheets.
In this tutorial, you’ll use FinanceJS to compute the periodic payment, then generate a full amortization schedule in JavaScript and visualize the result.
What “amortization” means in code
A standard fixed-rate, fully amortizing loan has three inputs:
- Principal (P): amount borrowed
- Annual interest rate (APR): e.g., 6.5%
- Term: e.g., 30 years
We typically compute monthly values:
- Monthly rate: ( r = \frac{APR}{12 \times 100} )
- Number of payments: ( n = years \times 12 )
The monthly payment is:
[
PMT = \frac{r \cdot P}{1 – (1+r)^{-n}}
]
FinanceJS provides this as PMT(rate, nper, pv) (with cashflow sign convention), so you don’t have to implement the formula manually.
Once you have the payment, each month follows the same rules:
- Interest payment:
interest = balance * r - Principal payment:
principalPaid = payment - interest - New balance:
balance = balance - principalPaid
Repeat for n months and you have the schedule.
Install and initialize FinanceJS
Node.js (recommended for quick testing)
npm install financejs
const Finance = require("financejs");
const finance = new Finance();
Browser usage (if you’re building a UI)
<script src="https://cdn.jsdelivr.net/npm/financejs/dist/finance.min.js"></script>
<script>
const finance = new Finance();
</script>
Build the amortization schedule function
Here’s a production-friendly function that returns:
- monthly payment
- total interest
- an array of schedule rows (month-by-month)
It also handles rounding and the “last payment” edge case to avoid negative balances due to floating-point drift.
function round2(x) {
return Math.round((x + Number.EPSILON) * 100) / 100;
}
/**
* Create a standard fixed-rate amortization schedule.
*
* @param {object} params
* @param {number} params.principal Loan amount (e.g., 250000)
* @param {number} params.apr Annual percentage rate, in percent (e.g., 6.5)
* @param {number} params.years Term in years (e.g., 30)
* @param {Finance} params.finance Instance of FinanceJS
*/
function createAmortizationSchedule({ principal, apr, years, finance }) {
if (!(principal > 0) || !(apr >= 0) || !(years > 0)) {
throw new Error("Invalid inputs: principal > 0, apr >= 0, years > 0 required.");
}
const n = Math.round(years * 12);
const r = apr / 100 / 12;
// FinanceJS cashflow convention: PV is negative for an outgoing loan amount,
// payment comes back as a positive number.
const paymentRaw = finance.PMT(r, n, -principal);
const payment = round2(paymentRaw);
let balance = principal;
let totalInterest = 0;
const schedule = [];
for (let month = 1; month <= n; month++) {
const interest = round2(balance * r);
let principalPaid = round2(payment - interest);
// Last-payment correction (or if APR=0 and rounding causes issues)
if (principalPaid > balance) {
principalPaid = round2(balance);
}
const endingBalance = round2(balance - principalPaid);
totalInterest = round2(totalInterest + interest);
schedule.push({
month,
payment,
interest,
principal: principalPaid,
balance: endingBalance,
});
balance = endingBalance;
// Stop early if fully paid (handles rounding)
if (balance <= 0) break;
}
return {
payment,
totalInterest,
totalPaid: round2(payment * schedule.length),
schedule,
};
}
Run a real example
Let’s generate a schedule for:
- Principal: $250,000
- APR: 6.5%
- Term: 30 years
const Finance = require("financejs");
const finance = new Finance();
const result = createAmortizationSchedule({
principal: 250000,
apr: 6.5,
years: 30,
finance,
});
console.log("Monthly payment:", result.payment);
console.log("Total interest:", result.totalInterest);
console.log("Payments:", result.schedule.length);
// Print first 3 rows as a sanity check
console.table(result.schedule.slice(0, 3));
If you’ve ever looked at a mortgage table, the output should look familiar: in early months, interest dominates; later, principal repayment accelerates as the balance shrinks.
Visualize the schedule
Numbers are useful, but amortization is easier to understand when visualized. Two practical charts:
- Balance over time (line chart)
- Principal vs interest (stacked bars or two lines)
Browser visualization with Chart.js
Include Chart.js:
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<canvas id="balanceChart"></canvas>
<canvas id="breakdownChart"></canvas>
Then:
function plotAmortization(schedule) {
const labels = schedule.map((row) => row.month);
const balances = schedule.map((row) => row.balance);
const interest = schedule.map((row) => row.interest);
const principal = schedule.map((row) => row.principal);
const ctx1 = document.getElementById("balanceChart").getContext("2d");
new Chart(ctx1, {
type: "line",
data: {
labels,
datasets: [{ label: "Remaining Balance", data: balances }],
},
options: {
responsive: true,
interaction: { mode: "index", intersect: false },
scales: {
x: { title: { display: true, text: "Month" } },
y: { title: { display: true, text: "Balance ($)" } },
},
},
});
const ctx2 = document.getElementById("breakdownChart").getContext("2d");
new Chart(ctx2, {
type: "line",
data: {
labels,
datasets: [
{ label: "Interest Portion", data: interest },
{ label: "Principal Portion", data: principal },
],
},
options: {
responsive: true,
interaction: { mode: "index", intersect: false },
scales: {
x: { title: { display: true, text: "Month" } },
y: { title: { display: true, text: "Amount ($)" } },
},
},
});
}
Call it after generating your schedule:
const { schedule } = createAmortizationSchedule({ principal, apr, years, finance });
plotAmortization(schedule);
Common pitfalls that break amortization schedules
Floating-point drift
JavaScript uses binary floating-point. Over 360 payments, tiny rounding errors compound. Rounding each month (and correcting the last month) prevents negative balances.
APR vs nominal rate confusion
Most calculators use APR/12 as the monthly rate for fixed-rate amortization. If you’re modeling more complex products, clarify whether you need effective annual rate conversions.
Fees and extras
Real-world loans often include origination fees, insurance, escrow, or balloon payments. The basic schedule here is the starting point—extend it by adding extra payments, fees per month, or irregular cashflows.
Where to take it next
Once you have the schedule, you can build real product features:
- Extra payment simulator (“pay $200 extra monthly—how much interest saved?”)
- Refinance comparison tool
- Early payoff date estimator
- CSV export + embedded charts for a user dashboard


Leave a Reply