Time to build the brain of our Purchase Tracker! While our React Native app looks pretty, it needs something to talk to — enter our Node.js TypeScript backend. Think of it as the reliable friend who remembers everything and never loses your receipts.
Why Node.js + TypeScript? Because Type Safety is Life 🌍
With Node.js + TypeScript, we get JavaScript everywhere plus compile-time error catching. No more "undefined is not a function" surprises in production.
Our stack:
- Node.js + Express + TypeScript 🕷️ — Type-safe web framework
- AWS Cognito 🛡️ — Authentication superhero
- PostgreSQL + Prisma 🗄️ — Relational database with modern ORM
- AWS S3 + Textract ☁️ — Cloud storage and OCR
- Jest + Supertest 🧪 — Testing squad
The Architecture 🏗️
Frontend (React Native) → Backend (Node.js/Express/TypeScript)
├── 🔐 AWS Cognito (Authentication)
├── 📸 AWS S3 (File Storage)
├── 👁️ Textract (OCR)
└── 🗄️ PostgreSQL + Prisma (Database)
Quick Setup 📁
npm init -y
# Core dependencies
npm install express cors helmet morgan dotenv joi multer prisma @prisma/client
npm install @aws-sdk/client-s3 @aws-sdk/client-textract aws-jwt-verify
# TypeScript setup
npm install -D typescript @types/node @types/express tsx nodemon jest
Project structure:
backend/src/
├── controllers/ # Request handlers
├── middleware/ # Auth, validation
├── routes/ # API endpoints
├── services/ # Business logic
├── config/ # Settings
├── types/ # TypeScript definitions
└── utils/ # Helpers
Authentication: AWS Cognito Magic 🏋️♂️
No more password hashing nightmares. Cognito handles auth so we focus on business logic.
// src/services/cognitoService.ts
import { CognitoJwtVerifier } from 'aws-jwt-verify';
class CognitoService {
private verifier = CognitoJwtVerifier.create({
userPoolId: config.aws.cognito.userPoolId,
tokenUse: 'access',
clientId: config.aws.cognito.clientId,
});
async verifyToken(token: string): Promise<any> {
return await this.verifier.verify(token); // ✨ Magic
}
async getOrCreateUser(cognitoPayload: any): Promise<any> {
const { sub: cognitoId, email, given_name, family_name } = cognitoPayload;
let user = await prisma.user.findUnique({ where: { cognitoId } });
if (!user) {
user = await prisma.user.create({
data: { cognitoId, email, firstName: given_name, lastName: family_name }
});
}
return user;
}
}
File Upload: S3 Made Simple 📸☁️
// src/services/s3Service.ts
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
class S3Service {
private s3Client = new S3Client({ /* config */ });
async uploadFile(file: UploadedFile, key: string): Promise<string> {
const command = new PutObjectCommand({
Bucket: config.aws.s3.bucket,
Key: key,
Body: file.buffer,
ContentType: file.mimetype,
});
await this.s3Client.send(command);
return `https://${config.aws.s3.bucket}.s3.amazonaws.com/${key}`;
}
generateFileKey(userId: string, originalName: string): string {
const timestamp = Date.now();
const sanitized = originalName.replace(/[^a-zA-Z0-9.-]/g, '_');
return `receipts/${userId}/${timestamp}_${sanitized}`;
}
}
Simple workflow: Upload → Generate unique key → Store in S3 → Return URL 🎉
OCR Magic: Textract Reads Receipts 👀📄
// src/services/textractService.ts
import { TextractClient, AnalyzeDocumentCommand } from '@aws-sdk/client-textract';
class TextractService {
private textractClient = new TextractClient({ /* config */ });
async analyzeExpense(s3Key: string): Promise<any> {
const command = new AnalyzeDocumentCommand({
Document: { S3Object: { Bucket: config.aws.s3.bucket, Name: s3Key } },
FeatureTypes: ['FORMS', 'TABLES'],
});
const response = await this.textractClient.send(command);
return this.parseExpenseData(response);
}
private extractTotalAmount(blocks: TextractBlock[]): number | null {
const patterns = [/total.*?[\$]?(\d+\.\d{2})/i, /[\$](\d+\.\d{2})/];
for (const block of blocks.filter(b => b.BlockType === 'LINE')) {
for (const pattern of patterns) {
const match = block.Text?.match(pattern);
if (match) return parseFloat(match[1]);
}
}
return null; // Sometimes receipts are mysterious 🤷♂️
}
}
Database: PostgreSQL + Prisma = Type Safety + Reliability 😊
// prisma/schema.prisma
model User {
id String @id @default(cuid())
email String @unique
cognitoId String @unique
firstName String?
lastName String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
receipts Receipt[]
purchases Purchase[]
}
model Receipt {
id String @id @default(cuid())
userId String
originalName String
s3Key String @unique
s3Url String?
status ReceiptStatus @default(UPLOADED)
extractedData Json? // OCR results
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id])
purchase Purchase?
}
enum ReceiptStatus {
UPLOADED | PROCESSING | PROCESSED | FAILED
}
Why PostgreSQL + Prisma rocks:
- ✅ Strong typing — Catch errors at compile time
- ✅ Relational integrity — Proper foreign keys
- ✅ Type-safe client — Auto-generated TypeScript types
- ✅ Migration system — Version control for schema
API Routes: Clean & RESTful 🛣️
// src/routes/receipts.ts
import { Router } from 'express';
import receiptController from '@/controllers/receiptController';
import cognitoAuth from '@/middleware/cognitoAuth';
import { upload } from '@/middleware/upload';
const router: Router = Router();
router.post('/upload', cognitoAuth, upload.single('receipt'), receiptController.uploadReceipt);
router.get('/', cognitoAuth, receiptController.getReceipts);
router.get('/:id', cognitoAuth, receiptController.getReceiptById);
router.delete('/:id', cognitoAuth, receiptController.deleteReceipt);
export default router;
Clean endpoints:
-
POST /api/receipts/upload
— Upload receipt image -
GET /api/receipts
— List all receipts -
GET /api/receipts/:id
— Get specific receipt -
GET /api/purchases
— List purchases -
GET /api/users/profile
— User profile
Error Handling: When Things Go Wrong 💥
// src/middleware/errorHandler.ts
import { Request, Response, NextFunction } from 'express';
const errorHandler = (error: any, req: Request, res: Response, next: NextFunction): void => {
console.error('Error:', error);
if (error instanceof AppError) {
res.status(error.status).json({
success: false,
code: error.code,
message: error.message,
timestamp: new Date().toISOString()
});
return;
}
// Prisma errors
if (error.code?.startsWith('P')) {
const status = error.code === 'P2002' ? 409 : 404; // Duplicate : Not found
res.status(status).json({
success: false,
code: error.code === 'P2002' ? 'DUPLICATE_RECORD' : 'RECORD_NOT_FOUND',
message: 'Database operation failed',
timestamp: new Date().toISOString()
});
return;
}
// Default error
res.status(500).json({
success: false,
code: 'INTERNAL_ERROR',
message: 'Something went wrong',
timestamp: new Date().toISOString()
});
};
Testing: Proving It Works 🧪
// tests/routes/receipts.test.ts
import request from 'supertest';
import app from '@/app';
describe('POST /api/receipts/upload', () => {
test('should upload receipt successfully', async () => {
const response = await request(app)
.post('/api/receipts/upload')
.set('Authorization', `Bearer valid-token`)
.attach('receipt', 'tests/fixtures/receipt.jpg')
.expect(201);
expect(response.body.success).toBe(true);
expect(response.body.data).toHaveProperty('id');
});
test('should reject unauthorized upload', async () => {
await request(app)
.post('/api/receipts/upload')
.attach('receipt', 'tests/fixtures/receipt.jpg')
.expect(401);
});
});
What we test: Happy paths, error cases, auth flows, edge cases.
Deployment Checklist 🌐
✅ Environment variables configured
✅ Database migrations run
✅ TypeScript compilation successful
✅ AWS services configured
✅ Tests passing
✅ SSL enabled
✅ CORS configured
Key environment variables:
NODE_ENV=production
DATABASE_URL=postgresql://user:pass@host:5432/db
AWS_COGNITO_USER_POOL_ID=us-east-1_xxxxxxxxx
AWS_S3_BUCKET=purchase-tracker-receipts
AWS_REGION=us-east-1
What We Built ✨
A backend that:
- ✅ Scales (handles real traffic)
- ✅ Type-safe (catches errors early)
- ✅ Secure (Cognito handles auth)
- ✅ Reliable (proper error handling)
- ✅ Maintainable (clean structure + TypeScript)
- ✅ Fast (optimized queries + caching)
TL;DR: Node.js + TypeScript + AWS + PostgreSQL + Prisma = Backend that makes you look like a rockstar 🎸
Stack: Node.js, Express, TypeScript, AWS Cognito, S3, Textract, PostgreSQL, Prisma, Jest
Architecture: Type-safe RESTful API with JWT auth, file processing, OCR integration, and modern best practices. 🚀
Top comments (0)