This content originally appeared on DEV Community and was authored by Artem Gontar
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. π
This content originally appeared on DEV Community and was authored by Artem Gontar

Artem Gontar | Sciencx (2025-09-05T12:06:00+00:00) Backend Implementation: From ‘It Works on My Machine’ to Production-Ready API. Retrieved from https://www.scien.cx/2025/09/05/backend-implementation-from-it-works-on-my-machine-to-production-ready-api/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.