CodeKits.devNew developer offer

React Expense Tracker with Node.js and MySQL

Build an expense tracker with a shared Node.js and MySQL API, precise money handling, secure accounts and predictable web and iPhone updates.

By Jonathan · 11 min read

Published

Expense tracker tutorial with a shared Node.js and MySQL backend
In this article

What you will build

Build an expense tracker with precise amounts, calendar-date filtering, authenticated accounts and a shared API. The React dashboard handles desktop and mobile web use.

This guide explains the architecture and implementation sequence. It is not a downloadable, production-ready application. The paired web and iPhone clients use the same authenticated API.

Define the shared product

Start with a deliberately small product. A user records an expense with an amount, a category, a description, and a spending date. The web dashboard and the iPhone application share the same backend, so a saved expense can appear on either device after refreshing. For this first release, use one currency per account. Do not add different currencies together or imply that a total includes currency conversion when no conversion system exists.

Trace one complete expense

Trace one expense through the whole system before building a large dashboard. The interface collects user input and sends a documented request. The server authenticates the caller, validates the values, and stores the record under that user's identity. The response returns the saved expense with its stable identifier. Only then should the interface report success. This end to end path gives you a useful baseline for testing both clients.

Store money without floating point errors

Choose a precise representation for money. In this example the account currency has two decimal places, and the API stores amounts as integer minor units. Twelve pounds and thirty four pence becomes twelve hundred and thirty four. Parse the input as decimal text rather than multiplying an arbitrary floating point value and hoping rounding fixes it. Define a maximum amount, reject extra precision, and document how refunds would be represented before adding them.

Separate spending dates from timestamps

A spending date and a creation timestamp answer different questions. Store the date selected by the user as a calendar date, while retaining a server timestamp for when the record was created. Validate real calendar dates rather than only checking the shape of a string. Filter a month using an inclusive start and an exclusive next-month boundary. This prevents a local date from moving into another month because a client converted it through midnight in a different timezone.

Design expenses and categories

Give expenses and categories explicit owners. Each expense refers to a category that belongs to the same user. A foreign key verifies existence but does not automatically establish that ownership rule, so enforce it during the server operation as well. Add indexes that match the user and month filters. Keep a version value if you want to detect edits from two devices, and use migrations rather than repeatedly resetting a database containing real records.

Agree on the API contract

Write down a small API contract that both clients can use. List the accepted fields, response shape, and error codes for each operation. The server derives the user identity from authentication, never from a user identifier supplied in the expense body. Keep list requests bounded and return a stable ordering. A separate summary endpoint can calculate the whole month's totals, which avoids accidentally presenting the total of only the current page as the monthly total.

Define the identity boundary

The browser collects credentials but the server decides whether they are valid. Keep user records and password hashes in the database, and return only the profile fields the interface needs. A successful login creates a new server session. Don't use a hidden page or a Boolean stored in the browser as your access control. Every protected request must be checked independently.

Design users and sessions

Use a stable user identifier, a consistently normalized email address, and a unique constraint to handle duplicate registration races. Store a password hash, never the original password. A separate session table connects a hashed random identifier to the user and an expiry time. Index the lookup fields and periodically remove expired rows. Database constraints complement application validation rather than replacing it.

Hash passwords deliberately

Validate password input and bound its length before expensive processing. This excerpt uses Node's asynchronous scrypt with a random salt and explicit work parameters. Store the salt, parameters, and derived value in a versioned format so you can upgrade them later. Benchmark the cost on your deployment hardware. During verification, derive the candidate hash using the stored settings and compare equal-length buffers with a timing-safe comparison.

Create a fresh session

After verifying the credentials, generate a fresh unpredictable session token. Save its digest on the server and send the original only in the session cookie. The cookie settings shown are for secure deployment. The database must enforce the expiry as well as the browser's cookie lifetime. Rotate the session when the user logs in, and revoke the previous identifier instead of reusing it.

Protect reads and mutations

Authentication middleware should resolve the session and reject expired or revoked entries before a handler accesses private records. State-changing routes also need a suitable request forgery defence, such as a correctly enforced origin policy and token strategy for your architecture. Add rate limits and monitoring around authentication. Never treat a cookie's presence alone as proof that a valid session exists.

Make logout invalidate access

Logout must remove the server-side session and clear the matching browser cookie. Clearing React state alone leaves a usable credential behind. Decide whether password changes should revoke every session, and provide that behaviour explicitly. Test logout by retrying a protected endpoint with the old session identifier. It must fail even if a client keeps a copy of the previous cookie.

Validate before writing

Validate the entire expense before writing it. Require a positive bounded integer amount for this initial expense-only workflow, check the account currency, and confirm that the category belongs to the current user. Limit description length and validate the date. Reject fields that the caller is not allowed to edit. Use parameterized database operations. These checks belong on the backend even when both the React form and the SwiftUI form already provide immediate feedback.

Create an expense safely

Create the expense using the authenticated user and validated values. Keep the category ownership check and insertion consistent inside the transaction, including appropriate locking or constraints for concurrent category changes. Return the saved identifier and normalized representation after the commit succeeds. If the connection fails around that point, the client may not know whether the operation completed. A request identifier and a deliberate retry strategy help prevent a double tap or network retry from creating duplicate expenses.

Make retries idempotent

Generate one request identifier for a particular intended creation and reuse it when retrying that same operation. Store the identifier with a uniqueness rule scoped to the authenticated user. If the request is repeated, return the original outcome rather than inserting a second expense. Compare the payload when resolving duplicates so the same identifier cannot silently refer to different content. Disabling the button improves usability, but database-backed idempotency protects against retries outside the interface.

Filter and paginate predictably

Apply the authenticated user and date range in the database query itself. Use a stable secondary identifier when multiple expenses share a spending date. Limit the page size and validate pagination inputs on the server. Keep the month definition identical between the list and summary endpoint. A chart and a list that disagree because they use different timezone or filtering rules will confuse users even when each individual query appears reasonable in isolation.

Calculate the full monthly summary

Calculate monthly category totals from all matching records, not just the visible page. Keep the user and date filters in the aggregate query. Database drivers may return large integer sums as strings, so define the response contract deliberately and avoid silently losing precision during conversion. Format the amount only at the presentation boundary. An empty month is a valid result with zero totals, and the chart should provide a readable text equivalent rather than relying on colour alone.

Build the React expense form

Keep the amount as text while the user edits it, then validate and convert it according to the shared decimal contract. Give every field an accessible label and display useful validation feedback beside it. Preserve the form when a request fails. After a confirmed save, update both the list and the monthly summary so the page remains consistent. Keep an explicit submitting state, and reuse the creation request identifier if the user retries the same intended expense.

Handle React request races

Changing a search query can create overlapping requests. Cancel obsolete requests and avoid letting an older response overwrite the latest results. Reset the page when the search changes, and show distinct loading, error, and empty states. Keep creation and editing controls disabled while their own requests are pending. A failed request should preserve the user's input rather than silently clearing it.

Handle edits from two devices

A web browser and an iPhone can edit the same expense. Use a version check when accepting a change so an old screen cannot silently overwrite a newer value. Keep ownership in the update predicate as well. If the version no longer matches, return a conflict and let the user reload or deliberately reapply their edit. Do not label every zero-row update as a permission failure; distinguish the relevant conditions through your documented server behaviour.

Keep category changes consistent

Category names may change without changing the identity of historical expenses. Decide what deleting a category means before adding the button. A simple first policy rejects deletion while expenses still reference it, or requires an explicit reassignment. Avoid an accidental cascade that removes financial history when someone cleans up a category list. Apply the same ownership checks to category reads and mutations, because an expense application can leak private information through supporting endpoints as well as through the main list.

Test isolation and calculation boundaries

Use two accounts to test the privacy boundary, including guessed expense and category identifiers. Verify calculations with small decimal amounts, large allowed totals, and an empty month. Check calendar boundaries and a leap day. Repeat the same creation request and confirm there is only one stored expense. Edit from two clients and verify that an outdated version produces the intended conflict. Inspect database state after rejected mutations instead of checking only the error message.

Deploy and verify the shared backend

Deploy the backend behind HTTPS and keep database access private. Use separate development and production configuration, protect secrets, and apply migrations with a recovery plan. Confirm that both clients point to the intended environment before testing synchronization. Create an expense on one client and refresh the other, then reverse the direction. Monitor failures without logging passwords, tokens, or unnecessary expense descriptions. Keep backups and practise restoring them into an isolated test environment.

Create an immutable release

Keep each release in its own directory and separate mutable data from application files. Upload into a new inactive directory, verify the artifact, then prepare dependencies and configuration links. Never overwrite the active tree incrementally while requests are being served. Retain the previous working release until the new one passes health checks and the rollback window has closed.

Activate and verify

On the Linux target, switch the prepared symlink into place and restart the appropriate service. Poll health with a bounded timeout and validate a response that reflects the application, not merely an open socket. If the check fails, restore the previous target and restart again, then verify recovery. This excerpt shows the activation concept; the full operational script needs error handling and deployment serialization.

Plan database compatibility

Application rollback doesn't automatically reverse database changes. Prefer compatible expand-and-contract migrations so the previous release can still run during the rollback window. Back up before risky changes and test restoration independently. Record deployment results and notify the responsible operator on failure. Avoid automatic destructive migration rollback unless its data consequences are fully understood and tested.

Continue the project

For implementation help with the web client, native app or shared backend, see CodeKits development services.

Reference documentation