Building a POS on a raw HTTP server
I deploy point-of-sale systems for a living, so I wanted to know exactly how one works — not the vendor's version, mine, down to the last byte. So I built a full POS with vanilla Node.js and nothing else: no Express, no React, no ORM. Just the http module, SQLite, and the fundamentals done by hand. Here's what that taught me.
The rule: no frameworks
Frameworks are how you ship fast — and exactly how you avoid learning what they hide. Express routes for you; React re-renders for you; an ORM writes your SQL for you. Every one of those is a black box I lean on daily without having built. So I made one rule for this project: the only runtime dependency is sqlite3. Everything else — routing, the request lifecycle, the frontend, the reports — I write myself. If it's slow or wrong, there's no library to blame.
Routing on the bare http module
Express makes app.get('/api/products', …) look like magic. Underneath it's a request coming into Node's http.createServer with a URL string and a method, and something deciding what to do with it. When you remove Express, that something is you: parse req.method and req.url, match it against your routes, read and buffer the request body chunks yourself, set the status and headers, and write the response. Do it once and the "magic" evaporates — a web framework is a router, a body parser, and a response helper, and now I've written all three. The server file owns routing, the API handlers, and the security checks in one readable place.
The barcode problem: EAN-13 check digits
A real POS scans barcodes, and generating valid ones is where the toy projects stop and the real one starts. An EAN-13 barcode isn't just 13 digits — the 13th is a check digit computed from the first 12 (weight the digits alternately by 1 and 3, sum, and take what's needed to reach the next multiple of ten). Get it wrong and no scanner in the world will accept the code. Writing that generator — and the scanner input that auto-detects an 8–13 digit numeric burst as a scan rather than typing — was the moment the project stopped being a CRUD app and started being a point-of-sale system.
State that can't lie: real-time stock
The unforgiving part of retail software is that stock is real. Two registers can't both sell the last unit. So the cart validates against live stock, every completed sale decrements inventory atomically, and low-stock thresholds surface before a shelf goes empty. It's the same discipline as the packet sniffer's bounded buffer — the data model has to reflect the physical world, or the software quietly lies to the person at the counter.
The feature nobody demos: backups
The part I'm quietly proudest of isn't the checkout — it's the backup system. Manual and scheduled SQLite snapshots, a 30-day retention policy that cleans itself up on startup, and a UI to list, download, and restore. It's unglamorous, and it's exactly the instinct my day job drills in: a store's data is the store. A POS that can ring up a sale but can't survive a corrupted database file isn't finished — it's a demo. Building backups in from the start is the difference between a project and a system.
What it taught me
Two things. First, frameworks are answers to questions you should be able to ask yourself. Having hand-rolled a router, I read Express's source differently now — I know what it's doing for me and, more usefully, what it's costing me. Second, production-readiness is a mindset, not a checklist. The tax config, the receipt fallbacks, the retention policy — none of them are hard, but choosing to build them is what separates "it works on my machine" from "it runs a store." That mindset is the throughline between this project and keeping 867 real ones online.