Freelance - Remote FullStack TypeScript Developer
Finvo | 04/2024 – Present
As a freelance FullStack TypeScript Developer for Finvo, I was entrusted with the end-to-end design and implementation of a cutting-edge, multi-tenant SaaS application aimed at automating invoice and bank statement processing for accounting professionals by leveraging AI and automation, ultimately providing them with unparalleled clarity and efficiency in their record-keeping and financial balancing tasks.
Architectural Foundation: A Scalable, Service-Oriented Approach
The system was architected with scalability and multi-tenancy as primary concerns. We adopted a service-oriented pattern, separating the frontend client from a robust backend API responsible for all data processing, AI integration, and persistence.
- Frontend: Built with Next.js, focusing on a dynamic and responsive user interface.
- Backend: Powered by NestJS, acting as the central processing hub.
- AI Layer: Integrated via Azure Form Recognizer for intelligent document analysis.
- File Handling: Utilized Uploadthing for secure and efficient file uploads.
- Data Storage: Employed a relational database (e.g., PostgreSQL) with a multi-tenant schema design.
This separation ensured that each part of the application could be developed, scaled, and maintained independently.
Backend Excellence with NestJS
The backend, built with NestJS in TypeScript, provided a solid and maintainable foundation. We leveraged NestJS's modular structure to organize different functionalities like authentication, user management, document processing workflows, and integrations.
Key responsibilities included:
- Designing RESTful APIs for frontend communication.
- Implementing secure authentication and authorization mechanisms, crucial for a multi-tenant application.
- Orchestrating the complex document processing pipeline.
- Handling database interactions and ensuring multi-tenant data segregation at the query level.
Here’s a simplified example of a NestJS controller endpoint that might handle document upload initiation:
// src/document/document.controller.ts
import { Controller, Post, UseInterceptors, UploadedFile, Body, Req, UseGuards } from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { DocumentService } from './document.service';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; // Assuming JWT auth
import { Request } from 'express'; // Using express Request for simplicity
@UseGuards(JwtAuthGuard) // Protect this route
@Controller('documents')
export class DocumentController {
constructor(private readonly documentService: DocumentService) {}
@Post('upload')
@UseInterceptors(FileInterceptor('file')) // 'file' is the field name for the uploaded file
async uploadDocument(
@UploadedFile() file: Express.Multer.File,
@Body('tenantId') tenantId: string, // Assuming tenantId is passed in body or derived from user
@Req() req: Request // Access user from request after auth guard
) {
// In a real app, tenantId would likely be derived from the authenticated user
const actualTenantId = (req.user as any).tenantId; // Example: get tenantId from auth payload
// Pass the file and tenant ID to a service for processing
const document = await this.documentService.processUploadedDocument(file, actualTenantId);
return { success: true, documentId: document.id };
}
- Intelligent Document Processing with Azure Form Recognizer
A cornerstone of the application was the integration with Azure Form Recognizer. This service allowed us to move beyond simple OCR and extract structured data (like invoice numbers, dates, line items, bank transaction details) from unstructured or semi-structured documents.
The process typically involved:
Receiving an uploaded document on the backend. Sending the document to Azure Form Recognizer for analysis using custom or pre-built models. Receiving the analysis results (JSON containing extracted fields). Parsing and mapping the extracted data to our internal data models. Implementing fuzzy matching and validation rules to handle variations and potential AI errors. Here’s a conceptual look at how a service might interact with the Azure SDK:
// src/azure/azure-form-recognizer.service.ts
import { Injectable } from '@nestjs/common';
import { AzureKeyCredential, DocumentAnalysisClient } from '@azure/ai-form-recognizer';
import { ConfigService } from '@nestjs/config'; // Assuming NestJS ConfigModule
@Injectable()
export class AzureFormRecognizerService {
private client: DocumentAnalysisClient;
private modelId: string; // Could be prebuilt or custom model ID
constructor(private configService: ConfigService) {
const endpoint = this.configService.get<string>('AZURE_FR_ENDPOINT');
const apiKey = this.configService.get<string>('AZURE_FR_API_KEY');
this.modelId = this.configService.get<string>('AZURE_FR_MODEL_ID'); // Get model ID from config
if (!endpoint || !apiKey || !this.modelId) {
throw new Error('Azure Form Recognizer configuration missing.');
}
this.client = new DocumentAnalysisClient(endpoint, new AzureKeyCredential(apiKey));
}
async analyzeDocument(documentBuffer: Buffer): Promise<any> { // Return type would be more specific
try {
const poller = await this.client.beginAnalyzeDocument(this.modelId, documentBuffer);
const result = await poller.pollUntilDone();
if (result.status === 'succeeded') {
// Process result.documents to extract needed data
const extractedData = this.extractDataFromResult(result);
return extractedData;
} else {
console.error("Document analysis failed:", result.error);
throw new Error(`Document analysis failed: ${result.status}`);
}
} catch (error) {
console.error("Error during Azure Form Recognizer analysis:", error);
throw new Error("Failed to analyze document with Azure Form Recognizer.");
}
}
// Helper to parse the complex result structure
private extractDataFromResult(result: any): any {
// Implement logic here to navigate the result.documents and extract
// fields based on your model's schema (e.g., invoiceId, total, date)
console.log("Analysis result (first document):", result.documents?.[0]);
// Example: Assuming a custom model extracts 'InvoiceId' and 'Total'
const document = result.documents?.[0];
if (document && document.fields) {
return {
invoiceId: document.fields.InvoiceId?.value,
total: document.fields.Total?.value,
// ... extract other fields based on your model definition
extractedFields: document.fields // Keep raw fields for review
};
}
return { extractedFields: {} }; // Return an object even if no fields found
}
}