Back to Zirve Asansör
PlateI

Backend — One REST Contract Over MySQL

I decided early not to use an ORM.

Role

The schema, the money rules and the contract are mine; I wrote the rules down before any code and reviewed every endpoint against them. The implementation was driven with AI coding agents. The sync idempotency, the push transport, Docker with Caddy and the JWT details are the parts I was learning as I went.

Stack

Node.jsExpressMySQLRaw SQLJWTbcryptnode-cronDocker
Backend — One REST Contract Over MySQL

Overview

I decided early not to use an ORM. I had Oracle SQL training, I could read a query faster than a model layer, and the schema was obviously going to change a lot. It did: 28 numbered migration files, applied in order from an empty database, and an API contract that went through thirteen revisions. It is Node and Express over MySQL, raw SQL through prepared statements, one JWT scheme for three roles. Money is a DECIMAL column and integer kuruş in code, so float rounding never touches a balance. The monthly dues are produced by a cron that does not ask what day it is. It asks whether this month's dues exist yet, and if the server was down on the first, it makes them when it comes back.

What it does

  • I28 SQL migrations applied in order. The schema is those files; there is no model layer
  • IIBalance computed on every request as valid dues minus valid payments. Never stored.
  • IIIPayments carry a type. Maintenance money settles dues, repair money settles repair charges, and the two never mix
  • IVA cron that asks whether the month's dues exist, so a week of downtime is filled in afterwards
  • VFourteen test suites; the database ones refuse to start unless pointed at a disposable database

Screens

The fifteen tables
The fifteen tables
Migrations applied in order
Migrations applied in order
The money suite
The money suite
Contract state tests
Contract state tests
Payment tracking tests
Payment tracking tests
Retention and receipts
Retention and receipts
The twenty-one step chain
The twenty-one step chain
A live period summary
A live period summary
The three invariants
The three invariants

Technical notes

  • IThe first allocation model was one pool. Every payment paid off the oldest open item, whether that was a maintenance month or a repair charge. It passed its tests and lasted a day. In practice it confused my father, because money handed over for a repair could land on an old maintenance month, so v2.11 gave payments a type and each debt its own walk, oldest first. A repair payment can't exceed the repair debt; a maintenance payment can, and the surplus is credit.
  • IIWhich months a payment closed is worked out at read time and never written down. That keeps the ledger honest, and it had a cost I didn't see coming: an old receipt can't be regenerated, because later payments and cancellations change the answer. The retention job would have pruned receipt PDFs after six months. They are now exempt and kept for good.
  • IIIThe test harness creates a database named zirve_test, runs there and drops it. A guard at the top of every suite refuses to start unless that is the target. I added it after realising that running a suite file directly would have fallen back to the .env database and written junk buildings into it.

What I learned

The obvious way to write the cron is wrong. Asking whether today is the first of the month means a server that was asleep on the first never creates that month's dues, and nobody notices until a balance comes out short. Asking whether this month has dues yet costs nothing extra and makes the job safe to run any number of times.