DEV Community

Cover image for Backend Implementation: From 'It Works on My Machine' to Production-Ready API
Artem Gontar
Artem Gontar

Posted on • Originally published at Medium

Backend Implementation: From 'It Works on My Machine' to Production-Ready API

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Project structure:

backend/src/
├── controllers/     # Request handlers
├── middleware/      # Auth, validation
├── routes/         # API endpoints
├── services/       # Business logic
├── config/         # Settings
├── types/          # TypeScript definitions
└── utils/          # Helpers
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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}`;
  }
}
Enter fullscreen mode Exit fullscreen mode

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 🤷‍♂️
  }
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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()
  });
};
Enter fullscreen mode Exit fullscreen mode

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);
  });
});
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)