Project: the Linkstash API end to end
The complete Linkstash architecture in one diagram: every route and decision from this series, tied together and ready for you to build your own version.

Thirteen lessons ago, a backend was three vague jobs: route, authenticate and validate, talk to the database. Here's what those three jobs actually became, all at once, in one running app.
The whole thing, in one diagram
POST /auth/register → 201 {id, email} | 400 | 409
POST /auth/login → 200 {token, user} | 401
GET /links → 200 {data, limit, offset} | 401
POST /links → 201 Link | 400 | 401
GET /links/:id → 200 Link | 401 | 404
DELETE /links/:id → 204 | 401 | 404
GET /boom → 500 (deliberate)That's the full route table you've now built from every angle: the routing that matches a request to a handler (lesson 3, lesson 4), the middleware chain every one of those routes walks through (lesson 5), the status codes chosen for a reason, not a guess (lesson 6), the error handling underneath all of it (lesson 7), and the database and validation that back every write (lessons 8 and 9).
Every box in that diagram is something you've read the actual source for, not a stand-in. requireAuth is the function from lesson 5, reading req.get("authorization") and calling userIdForToken. auth.ts and links.ts are the exact modules quoted across lessons 8 through 11. The error middleware at the bottom is the four-argument function from lesson 7, catching whatever /boom (or a real bug) throws.
The three decisions worth remembering after the code fades
Most of what you read here is ordinary CRUD, and ordinary CRUD is forgettable on purpose, it's supposed to be boring and predictable. Three decisions in this app aren't ordinary, and they're worth carrying into whatever you build next, independent of Express or SQLite or any of the specific tools:
Another user's data returns 404, not 403. GET /links/:id treats "doesn't exist" and "exists but isn't yours" as the same response, because confirming a resource exists to someone who can't access it is itself a leak. Any time you're building an endpoint scoped to an owner, this is the default worth reaching for.
Registration inserts first and catches the constraint violation, never check-then-insert. registerUser has no SELECT before its INSERT. The database's own UNIQUE index is the guard, because a check done in application code and a write done a moment later can always be raced by a second concurrent request landing in between. Whenever "does this already exist" needs to hold under concurrency, let the database enforce it and catch the failure, don't check first and hope nothing changes.
verifyUser spends real time on a login for an email that doesn't exist. The dummy-hash argon2.verify call that never affects the result exists purely so an unknown email doesn't answer suspiciously fast. Any comparison that reveals something through its timing, not just its result, deserves the same treatment: make the slow path and the fast path cost the same.
These generalize past Linkstash
Swap SQLite for Postgres, Express for Fastify, argon2 for bcrypt, and all three decisions above still apply exactly as written. They're not framework quirks, they're the actual reasoning a backend needs regardless of what's rendering the JSON.
It's tested, not just written
Nineteen tests back every claim this series made about Linkstash's behavior, not as an afterthought but as the thing that caught the bugs a plausible-looking first draft would ship. Six in auth.test.ts cover hashing, the duplicate-email race, and the login timing defense. Eight in app.test.ts cover the full HTTP surface, including the exact 404-not-403 and negative-limit-clamping behavior you read in lessons 6 and 12. Five in links.test.ts cover the database layer directly, including a literal SQL injection payload as a search term, asserting it matches zero rows instead of leaking the table.
That's not incidental to this series, it's the reason every code sample in these fourteen lessons could be quoted verbatim instead of written fresh for the page. Code you're confident in is code you can point at directly. Why write tests picks up from here and builds that same confidence into whatever you write next.
Build it yourself
You've read every meaningful line of this app across fourteen lessons, spread out and explained one decision at a time. The fastest way to make it stick is to stop reading it and build your own version, small on purpose: register a user, log in, save a link, list it back, delete it. Add one thing Linkstash doesn't have, tags on a link, or a PATCH /links/:id to edit a title, and make the same decisions this series walked through: where does validation happen, what status code actually describes what happened, what does an owner check refuse to leak.
Quick check
Which of these three deliberate Linkstash decisions is really about the same underlying idea, just applied in a different place?
This closes out the backend series. If security is next on your list, how apps actually get broken starts exactly where the 404-not-403 and password-timing decisions above leave off, with the rest of the ways a working API can still be broken into. Or head back to the full backend and APIs series to revisit any lesson.

Written by
Rhythm Bhiwani
Engineer and relentless builder, happiest reverse-engineering hard problems until they click.
Enjoyed this?
Tap the heart to leave some love.
Be the first to react
Comments
Join the conversation.
Loading comments…


