Portfolio Manifest — Prepared 2026-09-18
Full-Stack Engineer
An independently modernized fork of the RealWorld Conduit example app — a Medium-style publishing platform (CRUD, JWT auth, pagination) rebuilt one file at a time rather than copied over wholesale, with every dependency brought up to its current latest and every real bug found via a real failing test, not read twice and assumed fine. Backend and frontend both complete — the full stack runs end to end, verified live against a real Postgres database. This includes a chain of real, previously-invisible backend bugs found only by actually running it: three missing database tables for favoriting, following, and tagging, a Node-version incompatibility in the migration tooling, and a migration ordering bug — none caught by 228+ passing tests, since none of them exercise the real database path.
A dated example app, modernized for real
RealWorld's Conduit is a well-known "same app, N stacks" demo — a useful common ground, but the reference implementations age: pinned dependencies, an unmaintained CommonJS backend, no TypeScript, no CI, no tests. Rather than fork it wholesale, this repo rebuilds it file by file, upgrading every dependency and pattern to current latest as each file goes in, with the reasoning for every real decision written down as it happened — not reconstructed afterward.
Five route groups, four models, one middleware that never rejects a request for missing auth
verifyToken runs in front of nearly every route, but it's not
a gate: no Authorization header just means the request
proceeds anonymously — each controller decides for itself whether that's
acceptable. Real migrations are the only schema source of truth; nothing
patches the live schema at boot anymore.
bcrypt cost factor bumped 10→12 (current OWASP guidance). JWTs now
expire after 7 days and verification is pinned to
algorithms: ["HS256"] explicitly — the source issued
tokens that never expired. A global rate limiter now sits in front of
every /api route, closing 16 real CodeQL alerts at once
instead of patching each router individually.
The source relied on sequelize.sync({ alter: true })
at boot, which silently patched the live schema — exactly how two
missing foreign-key columns went unnoticed there. This repo's
migrations are authored from scratch and are the only thing that
creates schema now. See the
data model.
Found by a real failing test or a real CI run, not by reading the source twice
Every entry below was caught by something that actually ran — a test written to fail on the bug first, a CI job, or CodeQL — and every fix was re-verified by reverting it and watching the test fail again before being called done.
Comments association was copy-pasted from
Article.js and never corrected to the right key. Caught
and proven by a test written to fail on the original bug and pass on
the fix.
create-article/create-comment never
created the userId/articleId columns their
models' associations need — only worked in the source because
alter: true patched the live schema at boot. Every
migration now cross-checks its column shape against the real model
in its own test.
jwt.verify() accepted any
algorithm the token claimed. Fixed to a 7-day expiry and
algorithms: ["HS256"] pinned explicitly — both proven
by deliberately reverting each and watching the test catch the
regression.
return let request handling fall through to a
second next() call after an error response had already
been sent. Fixed and proven by reverting the fix and watching the
regression reappear.
error.message straight to API clients on any unexpected
error — a real information-disclosure gap, masked behind a generic
message instead.
deleteComment never checked that the comment being
deleted actually belonged to the article named in the URL slug.
Fixed to match the file's own established pattern.
app.get("/*any", ...) meant an unmatched POST/PUT/
DELETE fell through to Express's default HTML 404 instead of this
API's own JSON shape. Fixed to app.all(...), verified
by reverting and watching a POST come back with an empty body first.
Math.random()
to pick a random author. Fixed with one global
express-rate-limit instance ahead of every route mount,
and crypto.randomInt() in the seeder.
error.response from the Fetch
API's Response class, which has no
.data field — its assertion passed because of an
unrelated crash, not the code path it claimed to test. Fixed with a
real axios-shaped mock; proven by deliberately breaking the
extraction and watching all 5 cases fail first.
headers param at all,
unlike its siblings, even though the backend's GET-comments route
changes response shape based on whether a token is sent — comments
were always fetched as if the user were logged out.
getUser({ headers }).then((loggedUser) =>
setAuthState(...))
had no guard against an undefined resolution —
errorHandler can swallow certain errors and resolve
nothing, which would have overwritten a valid logged-in user with
undefined. TypeScript's
User | undefined return type on
getUser forced the fix.
{ headers: {...} | null; isAuth: boolean } let
TypeScript construct a state where isAuth was
true but headers was null —
checking one never narrowed the other, so every auth-gated component
needed its own defensive null check. Redefined as a discriminated
union; the repeated login guard itself was then extracted to a plain
helper, not a hook, since a hook would have to alert on every render
for a logged-out visitor instead of only on click.
56 more, smaller findings — a seeder that bypassed
bcryptHash() entirely, a missing
tagList default, a missing await on
setAuthor, two message-formatting bugs, a
FeedContext.tsx click handler reading
e.target instead of e.currentTarget, four
instances of the same trailing-space className bug across four different
components, an <a> nested inside a
<button> in ArticleAuthorButtons.tsx, a
missing return that let an unauthenticated comment-delete
request through with headers: undefined, a stale-closure bug
in a list's optimistic favorite-toggle update, a Link passed
an entirely wrong-shaped state object, three separate
components that navigated a user home on a silently-failed request as if
it had succeeded, three missing Sequelize join-table migrations that left
favoriting, following, and tagging silently broken against any real
deployed database, a react-paginate CJS/UMD interop crash the
moment the home page had real articles to paginate, an unparsed pagination
query value that could smuggle NaN into a live Sequelize
query, and more — are logged in full in
todo.md's phase-by-phase record, not summarized away here.
Live CI, not a static screenshot
Every push runs the full suite — ESLint, Prettier, TypeScript, and Vitest — plus a separate CodeQL security scan. The badges at the top of the repository's README are live, not decorative.
Controller and route tests run against a genuine in-memory SQLite Sequelize instance running the real models — not hand-stubbed mocks. See the testing strategy.
Every number on this page is reproducible from the repository's own todo.md phase log, commit history, and live CI runs.
Where the interesting decisions actually happened
The frontend was built bottom-up, the same way the backend was — leaf
modules (helpers, shared types, API services) before anything that depends
on them. That layer is done: all 16 service modules, then
AuthContext/FeedContext, both memoized only
after a review caught their Provider values causing unnecessary re-renders
on every consumer, then all 32 components, sequenced the same way — a full
dependency graph across the source's 31 components was mapped up front so
zero-dependency leaves got built before anything that composes them. Two
real architectural choices got decided rather than left open: TanStack
Query versus the source's hand-rolled fetch/loading/error pattern, settled
in favor of the latter after checking that the reference app itself has no
query-library dependency at all; and, once a second component needed the
same "reject an unauthenticated action" guard, a flat
AuthState interface that let isAuth and
headers disagree got redefined as a discriminated union, with
the duplicated guard itself extracted to a plain helper rather than a
custom hook — a hook would have to fire its alert on every render for a
logged-out visitor instead of only on click. That same helper-not-a-hook
reasoning held up through the whole rest of the components layer: when the
identical "resolve or fail" auth-callback logic showed up a second time in
the very last two components
(LoginForm/SignUpForm), it was extracted the
same way rather than left duplicated, precisely because no third
occurrence was ever coming. A third early call: a component's
fallback-prop logic was briefly changed from the source's
|| to ??, reasoning that || could
silently overwrite a deliberately empty string — technically sound, but
reverted once a check against the source's own real future callers showed
nothing in this app ever passes one; the deviation wasn't earned by an
actual need, so it didn't ship. That same discipline caught a subtler bug
near the end of the build: a simplification to AuthorInfo's
data-fetching effect looked like a clean win until a review checked it
against how a sibling component actually navigates between profiles and
found it would leave stale data on screen under a new username — reverted
to a more defensive version before it ever shipped, the same "verify
against real callers, not just plausible reasoning" standard applied
throughout. A process gap surfaced twice earlier in the build — the
backend's convention of a real GitHub Issue and board card per file
quietly lapsed for a handful of frontend files, not once but twice —
caught both times from a stale sub-issue count, backfilled both times, and
held for the rest of the components layer — until it lapsed a third time
during the route-pages and local-dev-setup work that followed, this time
across 12 consecutive commits during a long debugging stretch, caught by
re-reading the session's own commit history rather than assuming every
commit had been tracked, and backfilled the same way as the first two
lapses. Same standard, same discipline of fixing it in the open rather
than quietly moving on.
The route pages layer surfaced its own real finds:
Article.tsx
needed an empty-sentinel pattern for its article state, the third use of
the same technique first established for auth and settings forms;
main.tsx's router choice (hash-based, not path-based) was
verified rather than assumed, by actually reading how the Express backend
serves the built frontend and confirming it has no server-side fallback
route — the "wrong" router choice would have looked identical in
development and only broken on a production deep link; and the reference's
dead reportWebVitals boilerplate was deliberately not ported
after confirming, not assuming, that it was both inert as invoked and
incompatible with the pinned dependency version. Getting the whole stack
running for the first time was its own exercise in verifying rather than
guessing: a react-paginate crash was root-caused by reading
the package's actual minified build output, not just its type
declarations; three missing database tables were confirmed by inspecting
real Sequelize association metadata at runtime, not by trusting the model
source alone; and the new Docker Postgres setup deliberately defaults to
the standard port for anyone else cloning the repo, rather than hardcoding
this machine's own port conflict into a committed file. The still-open
gap: 62 of the 69 files tracked by issue #71 have no behavioral tests yet
(7 done so far), unlike the backend where every file has one — disclosed
and tracked on the board rather than quietly deferred.
A DRY/SOLID audit run across the whole codebase (2026-09-07) found 12
backend and 8 frontend real findings — not just duplicated code, but
genuine SRP, OCP, and DIP violations too: a shared
requireAuth
middleware replacing 10 reimplementations of the same auth check,
appendFollowers split apart once it turned out to be two
functions wearing one name, an authenticated user's email removed from a
mutable req.headers side channel, and a tangled
ArticleEditorForm.tsx pulled apart into a dedicated hook. All
20 findings were fixed. A second, narrower audit targeting test files
specifically (2026-09-18) found 9 backend and 2 frontend findings: all 9
backend findings and 1 of 2 frontend findings were fixed, and the second
frontend finding was reviewed and deliberately rejected as not a real
violation at this codebase's size.