Skip to main content

FinPay API

View Live Docs  |  GitHub Repository

FinPay is a production-style payment API built with Node.js, PostgreSQL, and Redis. The architecture is inspired by the backend patterns of Stripe, PayFast, and Yoco — focused on the correctness guarantees financial systems require: atomicity, idempotency, and audit trails.


Tech Stack

LayerTechnology
RuntimeNode.js 20
FrameworkExpress
DatabasePostgreSQL 15
ORMPrisma
Cache / Rate LimitingRedis 7
AuthenticationJWT + bcryptjs
LoggingWinston + Morgan
DocumentationSwagger / OpenAPI 3
ContainerisationDocker Compose

System Architecture

The diagram below shows how every inbound request travels through the system. Each layer in the middleware pipeline is a separate concern — if JWT auth fails, the request never reaches the rate limiter. If the rate limiter fires, the idempotency check never runs. If there is a cache hit, the database is never touched.

FinPay System Architecture

Middleware Pipeline — In Order

Every request passes through these four layers before reaching business logic:

1. JWT Authentication

Validates the Authorization: Bearer <token> header. If the token is missing, expired, or tampered with, the request is rejected with 401 Unauthorized before any other processing occurs. On success, the decoded userId is attached to the request object for downstream use.

2. Redis Rate Limiter — Three Tiers

All counters live in Redis, not in memory. This means the limits hold correctly whether you are running one Node.js instance or ten.

LimiterTargetLimit
GlobalAll /api routes100 requests / 15 minutes
Auth/api/v1/auth/*Tight limit — brute-force prevention
Transaction/api/v1/transactionsPer-user transfer cap

A request that hits the limit gets 429 Too Many Requests and stops here.

3. Idempotency Key Check

The X-Idempotency-Key header is checked against Redis before any handler runs.

Request with X-Idempotency-Key: abc-123

Redis lookup
├── Key found → return stored response immediately (no DB hit)
└── Key missing → continue, store result after processing

New keys are written to both Redis (TTL-expiring) and PostgreSQL (durable audit record). This is the same pattern Stripe uses — a client that retries a failed network request gets the original response, not a duplicate charge.

4. Redis Response Cache

Read-heavy endpoints — account balance, transaction history — are served from Redis without touching PostgreSQL on a cache hit. Only cache misses and write operations continue to the service layer.


Data Model

The schema is built around four tables with clear ownership:

FinPay Data Model

Each user owns exactly one account. Transactions reference the accounts table twice — senderId and receiverId — both wrapped in a single Prisma $transaction block so the debit and credit are always atomic.

Atomic Transfer — The Core Guarantee

src/services/transaction.service.js
const result = await prisma.$transaction(async (tx) => {
// Debit the sender
const sender = await tx.account.update({
where: { id: senderId },
data: { balance: { decrement: amount } },
});

// Reject if balance goes negative
if (sender.balance < 0) throw new Error('Insufficient funds');

// Credit the receiver
const receiver = await tx.account.update({
where: { id: receiverId },
data: { balance: { increment: amount } },
});

// Record the transaction
return tx.transaction.create({
data: { senderId, receiverId, amount, idempotencyKey },
});
});

If any step throws — insufficient funds, database connection lost, anything — Prisma rolls back the entire block. Balance never goes negative. Value is never lost between accounts. There is no partial state.


API Endpoints

MethodEndpointDescriptionAuth
POST/api/v1/auth/registerCreate user and wallet accountNo
POST/api/v1/auth/loginAuthenticate and receive JWTNo
GET/api/v1/accountsGet own account balanceYes
POST/api/v1/transactionsSend money to another accountYes
GET/api/v1/transactionsList transaction historyYes
GET/api/v1/healthService health checkNo

Full request/response schemas with interactive try-it-now at the Swagger UI.


Seed accounts:

EmailPasswordBalance
alice@finpay.devpassword123ZAR 10,000.00
bob@finpay.devpassword123ZAR 5,000.00

What This Project Demonstrates

Correctness under failure. Atomic transfers mean partial failures are impossible. The system either completes a transfer or it does not.

Distributed correctness. Rate limit counters and idempotency keys in Redis work correctly whether running one instance or ten.

Observability from the start. Structured logging and request tracing are part of the middleware pipeline from the beginning — not added after the fact.

Infrastructure as configuration. PostgreSQL and Redis are provisioned via Docker Compose. The entire infrastructure is version-controlled and reproducible with one command.


Roadmap

  • GitHub Actions CI pipeline — lint, test, build on every push
  • Containerised Node.js app — Docker image for the API
  • Deployment to Railway or Render with health check integration
  • Webhook events for transaction status changes