DEV Community

Farbod F.
Farbod F.

Posted on

Amortization math in 40 lines of JavaScript: building a mortgage calculator with zero backend

A mortgage calculator is one formula, a loop, and a rounding decision. The formula is the easy part. The rounding decision is the one your users will actually notice.

I built a mortgage calculator as a static page — no backend, everything in the browser. The core question it answers sounds trivial: given a loan amount, a rate, and a term, what fixed monthly payment brings the balance to exactly zero? The formula is closed-form, the code fits in under 40 lines, and yet the first version I shipped disagreed with every bank statement I tested it against — by cents. Cents, it turns out, are where calculator credibility lives.

The one formula

The payment that amortizes a loan is the present value of an annuity, solved for the payment:

M = P × r(1 + r)ⁿ / ((1 + r)ⁿ − 1)
Enter fullscreen mode Exit fullscreen mode

P is the loan amount, r is the monthly rate (annual ÷ 12), n is the number of payments. In JavaScript, that is one function:

function monthlyPayment(P, annualRate, years) {
  const r = annualRate / 12;
  const n = years * 12;
  return P * r * Math.pow(1 + r, n) / (Math.pow(1 + r, n) - 1);
}
Enter fullscreen mode Exit fullscreen mode

The schedule — what each payment actually does — is a loop around it:

function schedule(P, annualRate, years, extra = 0) {
  const r = annualRate / 12;
  const pay = monthlyPayment(P, annualRate, years) + extra;
  let balance = P, month = 0, interestPaid = 0;
  const yearly = [];
  while (balance > 0 && month < 1200) {
    const interest = balance * r;
    const principal = Math.min(pay - interest, balance);
    balance -= principal;
    interestPaid += interest;
    month++;
    if (month % 12 === 0 || balance === 0) {
      yearly.push({ year: Math.ceil(month / 12), balance: Math.round(balance * 100) / 100 });
    }
  }
  return { months: month, interestPaid: Math.round(interestPaid * 100) / 100, yearly };
}
Enter fullscreen mode Exit fullscreen mode

That is the whole engine: payment, interest-on-remaining-balance, principal as the remainder, repeat until zero. No backend, no library, no state beyond a balance variable.

Worked example: $300,000 at 6.5% for 30 years

Inputs: P = $300,000, r = 0.065/12, n = 360.

  • Monthly payment: $1,896.20
  • Total paid over 30 years: $682,633.47
  • Total interest: $382,633.47 — more than the loan itself

The first payment breaks down as $1,625.00 in interest and only $271.20 in principal — 85.7% interest. That is not a bank trick; it is what "interest on the remaining balance" means when the balance starts at $300,000. One month's interest on the full amount is $1,625, whatever you do. Each payment shrinks the balance slightly, so next month's interest charge is slightly smaller, and slightly more of the payment reaches principal. The ratio flips slowly, then all at once in the final years.

The figure that surprises everyone

Five years in, you have made 60 payments totalling $113,772.25. Guess the remaining balance: $280,832.93. You have repaid only $19,167.07 of principal — 6.4% of the loan — while paying $94,605.18 in interest. A sixth of the way through the loan in time, and the balance has barely moved.

That front-loading is also why extra payments are so powerful early. Add just $200 a month to the same loan:

  • Payoff in 277 months (23 years 1 month) instead of 360 — about 6 years 11 months early
  • Total interest falls to $279,184.67
  • Interest saved: $103,448.80

Two hundred dollars a month — roughly a 10.5% bump on the payment — erases more than a quarter of the total interest. The same $200 in year 25 mostly replaces payments you would make anyway. Extra principal attacks the balance when the interest charge is largest, which is exactly when the balance is largest: at the start.

(Shortening the term does the same thing from the other direction: the same $300,000 at 6.5% over 15 years costs $2,613.32 a month — only 1.38× the 30-year payment — but total interest collapses from $382,633.47 to $170,397.98.)

The cents problem

So why did my first version disagree with bank statements? Because a bank rounds every month's balance to cents, and my loop did not. Run the loop in raw float64 and the final balance lands a few cents from zero; a statement, rounding monthly, lands exactly on zero with a slightly different interest total. Neither is "wrong" — they are different rounding policies — but a user comparing your calculator to their statement will trust the statement.

The rules I shipped with:

  1. Round the displayed balance to cents every month, exactly like a ledger does. Do not round intermediate interest to cents and then feed it back into a second calculation — pick one policy and document it.
  2. Clamp the final payment. Math.min(pay - interest, balance) is the line that keeps the last payment from overshooting and the balance from going negative by $0.03. Without it, your schedule ends with a phantom final month.
  3. Never compare the balance to zero with ===. After 360 float operations, balance === 0 is a coin flip. Loop while balance > 0.005 (half a cent) or, better, do the whole schedule in integer cents.
  4. Round once at display, or in cents throughout — never both inconsistently. Mixing a cents-rounded balance with a float-rounded interest total is how you get a schedule whose column sums disagree with its own total by a dollar.

Honest caveats

  • This is principal and interest only. A real monthly housing payment adds property tax, homeowner's insurance, and possibly PMI — often 25–40% on top of the P&I figure.
  • Rates here are illustrative. Your rate, down payment, and term change every figure above. The shape — interest-heavy early, principal-heavy late — never changes for a fixed-rate loan.
  • Extra payments are not always optimal. If your rate is below what you would earn investing, or you lack an emergency fund, extra principal may not be the best use of the money. This article shows what the math does, not what you should do.
  • Adjustable-rate loans differ. Everything here assumes a fixed rate. ARMs reset, and the schedule has to be rebuilt at each reset.

Run your own numbers

Plug in your price, down payment, rate, and term, and see exactly where every dollar of every payment goes:

Mortgage calculator — free, no signup, runs entirely in your browser.


The bank already ran these numbers. Now you have the loop too.

Top comments (0)