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)