Reflection

Financial Web App Reflection

I recently created a web app for tracking financial actions. It allows users to login, create accounts, budgets, and transactions, as well as visualize their weekly and monthly spending. I was motivated in part due to a desire to get a handle on spending, but also as an excuse to get my feet wet with a modern web development stack. The stack is React, Tailwind, and Material UI on the client side; Node, Express on the server side; PostgreSQL as a database; Nginx as a web server and reverse proxy; and Digital Ocean as a self-hosted Linux server. Although these choices are admittedly arbitrary, they demonstrate broad industry acceptance, and similar technologies such as Svelte, MySQL, Django, Firebase, etc. would pose little trouble if one wanted to pivot.

This document will outline every major component that went into the app’s development, including how HTTP requests are received and handled on the client and server, the React hooks that were used, the database, and the production environment.

Request Data Flow

Pages

#### Client: This web app can be considered a single page application (SPA), meaning the same HTML page is returned regardless of which endpoint is hit. Once this page is rendered, the React Router library creates a JSON representation of the browser’s document object model (DOM), known as the virtual DOM, compares this with the actual DOM (initially empty when hitting endpoints), and injects HTML into the rendered page according to the virtual DOM.

#### Server: In production, the web server is configured to serve the single html page for requests that are not static files nor back end requests (which would leave endpoints as the remaining option). In this app, the Nginx configuration file proxies any endpoint beginning with /api to the Node port, serves static files such as images or JavaScript directly, and falls back to serving the single HTML document index.html in any other case.

Back End

Client:

SPAs are appealing in part due to their lack of page loads. This is possible via AJAX requests, which are asynchronous HTTP requests that don’t require page reloads for their response to be realized. Most requests in a React app are AJAX, meaning pages reloads are seldom. When a response is received, the virtual DOM is updated with the new content based on how the React component is written to react to this response. Then, the actual DOM is compared with this new virtual DOM, and the only changes to the HTML are the changes that the response elicited. No page load required.

React components contain the AJAX requests, including the request body, as well as what action will occur when the response is received. Since React manages the virtual DOM diffing and raw HTML injection, the server very seldom returns HTML (Ignoring server side rendering, which is out of scope for this project). Instead, the response is in the form of JSON, which can include status, data, error logs, etc., and HTML creation and rendering is handled client side. This is a slightly new paradigm when compared to a non-SPA. Traditionally, a client would be served HTML directly and render this response body. Using React, the practice is instead to use the JSON response to create new React components, which will be picked up by React, added to the virtual DOM, diffed with the actual DOM, and injected as HTML, all client side.

Server:

Since HTML is created dynamically client side, back end requests are typically only for database reads/writes and authentication. As mentioned, these requests would begin with /api in its request path, and would then be proxied to the Node server. The Node process is being run from the server.js container file, which establishes a port for Node to listen over, as well as app-wide middleware leveraging Express. This middleware includes CORS to bypass browsers enforcing SOP on the front end hitting the back end in development, a cookie parser for authentication, and the different routes the app uses to partition concerns. For this app, it includes routes such as accounts, budgets, authentication, etc.

The /routes directory are where routes for the respective concerns are defined, including the endpoint path, the request type, and the function used for business logic and querying the database. The /controllers directory contains the implementation of said business logic, including authorization, getting accounts, creating transactions, etc. If these functions need access to the database, they call the corresponding function in the /models directory, which contains the actual SQL statements for querying the database. It accesses the database via a pool established in db.js, which expects a PostgreSQL server to be running and accessible via the database URL in the .env file.

Hooks

State

State allows JavaScript variables to trigger the component it’s a part of to it’s a part ofen it changes value. For example, the list of transactions is not available immediately on render, thus its state is initialized to an empty array. When the response is received (which is very quick, but still after), the state changes to the full array of transactions. Since the transaction list uses the State hook, the entire transaction list component is re-rendered with the updated transactions array.

Effect

Effect allows for a piece of logic to run when the state of something changes. Its body always runs when a component is first rendered, also known as when a component ‘mounts’, and it will re-run when its arguments change state. For example, the bar graph that can toggle between weekly and monthly budgets in the budgets endpoint contains an effect hook. It runs initially when the page mounts (either a page load of an endpoint is reached for the first time). Since it is provided period as an argument, every time the state of period changes (weekly -> monthly or vice versa), the code that renders the chart runs again with the new updated period.

Context

A key motivation behind React’s component approach is separation of concerns. A component should loosely serve one purpose and be minimally coupled with others. Sometimes though, certain information has to be shared to some extent among a collection of components. One way this can be achieved is by passing variables as parameters (props) to components. However, with a sufficiently complicated component structure, this can result in passing arguments to components that do not need them, but are a parent of a component that needs them (prop drilling). This is considered bad practice.

The solution is to use the Context hook. This allows state to be shared among a group of components without prop drilling. This is achieved via a particular design pattern.

For example, if there was no context hook, when a user submits the transactions creation form, only the form component is triggered to re-render. The transaction list cannot receive this information and only re-renders after a page load or some other trigger. Instead, the component TransactionsContext.jsx wraps the root of the DOM, which contains the fetch request for the transaction, as well as the state hook for the transaction list itself. Since it is wrapped in a parent of both the transaction form and the transaction list, the fetch request can create new transactions, as well as update the state of the transaction list, while keeping the concerns of the respective component files separate. Then, the components simply call the function or reference the state in the context provider for adding transactions or listing them.

Authentication

Registration

When a user creates a user name and password, the password is hashed and stored as such in the database. This means that even if the database is compromised, the passwords would be useless without having the actual passwords that solve the hash. There should be some sort of recovery protocol for forgotten passwords, however, the scope of this project never reached that point. As it stands, if someone forgets their password, the account is locked indefinitely.

Login

When a user logs in, their password entry is used to solve the hash associated with their username entry. If it works, they are granted access to their account. The app is using Bcrypt, which is a strong hashing algorithm based on the blowfish cipher. By design, it is computationally intensive to brute force.

Cookies and Tokens

When a valid password is received on a login attempt, the app checks if they have a refresh token in the database. A refresh token is a long term access token that allows for users to stay logged in when they close the tab. Without this, every time a user visits the site or even reloads their browser while remaining on a page, they will be prompted to login again. If the user has a valid refresh token in the database, which is the case when users log out, a new one will not be minted. If they are logging in for the first time, or it is passed token’s expiry date (in this app’s case it’s seven days), a new one will be minted. In both cases, this refresh token will be stored as a cookie in the browser, meaning the user can visit the page again or reload it without having to login.

In addition to a refresh token, an access token is also minted on login. This token is shorter lived than the refresh token (in this app’s case it’s 15 minutes) and only stored as a cookie, not in the database. This token is used to authenticate users every endpoint it hits. The intention behind a token like this is to limit exposure of the more powerful refresh token. It’s better to expose a shorter-lived token, as an attacker would only then obtain, in this case, 15 minutes of access. It’s also computationally cheaper process to use, as it does not require a database query, which is important for something that will be called often.

Every protected endpoint hits the /me endpoint. This checks if there exists a valid access token. This checks out because we know these were minted relative to the refresh token, which is tied to a successful login attempt. If this fails, it hits the /refresh endpoint. This attempts to mint a new access token if a refresh token cookie exists. If there is a refresh token, a new access token is minted. If not, the user must login again. This will only happen after a week of being logged in, and is the intention of the lifespan of the refresh token.

Database

This app uses the relational database software PostgreSQL. This is a database server, as opposed to something like SQLite, thus it must listen on some port. In db.js, a database pool is created relative to the database URL in the server .env file. This allows the app to hit the port the database is being run on. As mentioned in the request flow section, this file expects a PostgreSQL server to be running and accessible via the database URL in the .env file. PostgreSQL was chosen in part to experiment with a server-based database, as well as support for multiple concurrent users.

The database contains tables for users, accounts, categories, transactions, budgets, and refresh_tokens. The schema is normalized with the exception of the balance column in the accounts table. I did not catch this in the beginning and is one of the things I intend on refactoring.

Production

This app is deployed on a Digital Ocean droplet, which is a remote Linux machine commonly used for self hosting Linux servers. On this server, there is an Nginx process running that acts as a web server and a reverse proxy. A web server means it can accept HTTP requests over the internet, and a reverse proxy means it doesn’t expose the actual endpoints; rather it proxies them to the appropriate parts of the app, whether it’s the Node server or static files.

In a production environment, it is best practice to employ a service manager so the app can handle errors that would otherwise crash a development environment. This app uses pm2, which is a process manager that spins up multiple Node services and handles errors/crashes. Nginx handles concurrency very well and thus works well with pm2. Nginx efficiently allocates requests to different workers/Node processes, allowing for multiple requests to be handled concurrently.

As mentioned in the request data flow section, there is an Nginx configuration file that proxies requests. Here’s part of it:

    server_name finance-app.mattdehaas.dev;

    root /var/www/projects_site/finance-app/client/build;
    index index.html;

    # Serve React front end
    location / {
        try_files $uri /index.html;
    }

    # Proxy API requests to Node back end
    location /api {
        proxy_pass http://localhost:5001;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
    }

The location keyword here is the HTTP request path. Here, if the path begins with /api, the request will be proxied to the port serving the Node app. In any other case, if the path is a static asset on the server, such as an image or the JavaScript build file, it serves this file directly. Otherwise, it falls back to index.html, which is the shell HTML file mentioned earlier.

The Digital Ocean droplet being used for this app is very minimal (1GB RAM), and building the React app for production is sufficiently expensive that it is not runnable on the remote. Thus, there is a small deploy shell script to build the React build directory, rsync the files over to the remote, and restart the process manager.