Store Money as Integer Cents
The form displays amounts such as 84.50, but the application should not use decimal JavaScript numbers for financial arithmetic. JavaScript uses binary floating-point numbers, so some decimal values cannot be represented exactly.
For this project, the simple rule is: the stored value is an integer number of cents; the formatted value is only for display.
Open client/src/lib/finance.js and create the conversion used at the form/API boundary:
export function parseAmountToCents(value) {
const text = String(value ?? "").trim();
if (!text || !/^\d+(?:\.\d{1,2})?$/.test(text)) return null;
const [whole, fraction = ""] = text.split(".");
const cents = Number(`${whole}${fraction.padEnd(2, "0")}`);
return Number.isSafeInteger(cents) && cents > 0 ? cents : null;
}
Here is what each part does:
String(value ?? "")makes the helper safe when the input is missing.- The regular expression allows whole amounts or amounts with one or two decimal places.
split(".")separates dollars from cents.padEnd(2, "0")turns5cents into50cents.- Joining the two strings gives an integer such as
8450for$84.50. Number.isSafeInteger()prevents values outside JavaScript’s safe integer range.> 0rejects zero and negative values because this application records positive transaction amounts.
Then change the transaction object so it carries amountCents instead of amount. The UI can turn that integer back into an input value with centsToInput() and can display it with formatCents().
The server must enforce the same shape. Open server/src/domain/transactions.js and validate:
if (!Number.isSafeInteger(amountCents) || amountCents <= 0) {
return "amountCents must be a positive integer";
}
The server is the final authority because a request can arrive without using this React form.
The project also contains older sample data that used an amount field. The persistence layer migrates that value once by multiplying it by 100 and then removes the old property. That lets an existing learner database move to the new model without manually rewriting every record.
Test it
Run the application and create an expense for 84.50. Inspect the stored JSON file and confirm the transaction contains:
amountCents: 8450
Also verify that 84.567 is rejected by the form or validation helper.
Checkpoint
You can now explain the difference between a display amount such as 84.50 and the stored value 8450, and every layer of the application uses the integer-cent representation.