This series's previous article saw us build an OAuth authentication flow via GitHub. In this article, we'll continue adding APIs to the backend and specifically, implementing all the APIs related to transaction management while sticking with "our" modified MVC design pattern. Let's dive in.

We assume you've gone through the previous articles in this series where we built the app's utility server and the authentication flow. If not, kindly check them out. Also, this article requires that some modules are installed:

  • busboy - module for parsing incoming HTML form data (handling file upload)
  • csv-parse - CSV parser for Node.js and the web (latest version doesn't need additional @types/ installation)
  • ws - a Node.js WebSocket library

The following command will install them as well as their types:

Terminal
sh
Sirneij
Sirneij/finance-analyzer
00

An AI-powered financial behavior analyzer and advisor written in Python (aiohttp) and TypeScript (ExpressJS & SvelteKit with Svelte 5)

sveltetypescriptpythonjavascriptcss3html5
Note: Exclusion of interfaces/types

For brevity, we won't be including type declarations in this article and subsequent ones. You can always get them from the types directory.

We'll start by writing out our modular file parsers. There will be a shared BaseParser:

src/utils/file.utils.ts
ts

It is an abstract class that implements the IFileParser interface. It provides a shared structure for file parsers by:

  • Storing a user ID (needed for transaction filtering)
  • Declaring an abstract parse() method that concrete subclasses must implement to parse a file buffer into transactions.
  • Including a helper method, mapToTransaction(), which converts raw transaction data into a Partial<ITransaction> object, handling type conversions (e.g., converting a date string to a Date object, converting amount and balance to numbers, and ensuring the transaction type is in the correct string format).

All the other parsers would inherit this:

src/utils/parsers/csv.parsers.ts
ts

This class extends the shared parser (BaseParser) to reuse common functionality while implementing CSV-specific logic. It uses the csv-parse library with options (like relaxed column count and skipping empty lines) to handle potentially inconsistent CSV data. While it implements helper functions (cleanDescription and normalizeAmount) to sanitize and convert raw CSV data, it throws an error immediately if required CSV fields are missing, ensuring data integrity. It also tries to dynamically determine the transaction type based on keywords in the CSV (credit implies income).

Next is the PDF parser:

src/utils/parsers/pdf.parsers.ts
ts

As mentioned in the second article of this series, our AI service (utility) server will handle parsing PDF files since better tooling is available in the Python ecosystem for that, this parser uses an external API (our utility server API) to extract text from PDFs via a POST request. Then it updates the current year based on the date headers found in the text. Finally, it uses a regular expression to extract transaction details from each line and constructs transaction objects thereof.

To finish off the parser implementation, we need a nice way to automatically detect and use the right parser when a file is provided. For that, we have:

src/utils/parsers/factory.parsers.ts
ts

It implements a factory pattern that selects the correct parser based on the file's MIME type. The getParser() method returns a CSVParser or PDFParser instance (or errors for unsupported types), while isSupportedType() verifies if a MIME type is among the accepted ones.

Before implementing other business logic, let's create a transaction schema and model so that MongoDB can help manage the data:

src/models/transaction.model.ts
ts

Just a simple schema with our defined columns.

Tip: Mongoose's automatic inclusion of date columns

When the timestamps is set to true, mongoose automatically adds createdAt and updatedAt to the schema. If you don't want that behavior, excluding it and giving it a false value is all you need.

We'll proceed to writing our business logic in:

src/services/transaction.service.ts
ts

This service class orchestrates all transaction-related operations by:

  • Parsing files with the appropriate parser (via ParserFactory) and inserting the parsed transactions.
  • Fetching (with pagination) transactions, summarizing financial data, and analyzing spending via our utility service endpoints.
  • Creating and deleting transactions.
  • Establishing a WebSocket connection to forward real-time updates from our utility server to the front end.
Tip: Better query fetching with pagination

For fetching transaction data with pagination, we are currently using two queries: find and countDocuments. We can optimize this by using a single query aggregation pipeline.

Next, we write the controllers.

Here we provide the controllers for the transaction:

src/controllers/transaction.controller.ts
ts

This controller class handles HTTP requests for transaction-related operations. It processes file uploads (using busboy) by delegating to TransactionService, and exposes endpoints for fetching (with pagination), summarizing, analyzing, creating, and deleting transactions—all with appropriate error handling.

The last step is coupling this together. Let's first create the routes:

src/routes/transaction.routes.ts
ts

And lastly, register it with with app:

src/app.ts
ts

We will stop here in this article. In the next one, we will complete this transaction service by writing the WebSocket handler. Then, we will incept the front end with SvelteKit by implementing the authentication flow, and user dashboard with TailwindCSS v4.

Enjoyed this article? I'm a Software Engineer and Technical Writer actively seeking new opportunities, particularly in areas related to web security, finance, healthcare, and education. If you think my expertise aligns with your team's needs, let's chat! You can find me on LinkedIn and X. I am also an email away.

If you found this article valuable, consider sharing it with your network to help spread the knowledge!