Projects DevQuill case study

DevQuill

My public developer journal. A full MERN app I took from a blank PRD to production, then hardened with refresh tokens and CI.

● Live since Jan 2026Solo projectTypeScript end to end🔒 Private repo
Role
Everything: PRD, design, API, client, deploys
Timeline
Nov 2025 – Apr 2026
Stack
React 19 · Express · MongoDB · Vitest
Hosting
Vercel · Render · MongoDB Atlas
Pinned
djerma · Jan 28Welcome to DevQuill — I'm Building This in PublicHello there, stranger. I'm Johnny — automation enthusiast, an ESL teacher, and someone who's trying to become a software developer. That last p…welcomeabout-me

The pinned post on DevQuill.

In short

  • WhatA blog with public reading pages and a private dashboard where I write posts in Markdown.
  • Who forMe first. Readers are developers, learners and recruiters.
  • Built12 API endpoints, 3 data models, token-based auth, a CI pipeline, two hosting platforms.
  • LearnedWhat it takes to ship and run a full-stack app, not just write one.

Why it exists

I was reading Medium a lot and wrote a few articles there. But I wanted something of my own: a place I built myself, to track what I learn and to build in public.

It was also a deliberate learning project: take a product from idea to production on my own, with a written PRD, real deployments and a pipeline I can explain.

And it would double as my personal website, built by me from the first line.

How it's built

An npm-workspaces monorepo. Vercel serves the React app as static files, and the browser calls the Express API on Render directly. Both platforms deploy on their own when I push to master.

Data model

Useremail uniquepasswordHash bcryptname role admin | author
Posttitle · content Markdownauthor → Userstatus draft | publishedtags string[]slug uniquepinned · pinnedAt · publishedAt
RefreshTokentoken SHA-256 hashuserId → UserexpiresAt +30 daysisRevoked

Auth, step by step

Press a step or let it play.

ClientBrowser · React + Axios
access JWT15 min
refresh token30 days
POST /auth/login
access + refresh
ServerExpress APIbcrypt checks the password ✓
DatabaseMongoDB · RefreshTokenno session yet

01 · Log inbcrypt checks the password. The API returns a 15-minute access JWT and a 30-day refresh token.

I launched with a single 7-day JWT, then replaced it with this design, which cuts the exposure window from 7 days to 15 minutes. Existing logins were migrated on first load, so nobody got logged out.

Key decisions

What I chose, what I gave up for it.

Hosting

Vercel + Render + Atlas, all free tier

Why: zero cost, auto-deploy from GitHub, very little DevOps. I compared Railway, Netlify and DigitalOcean first.

Trade-off: Render sleeps after 15 minutes idle and needs ~30 seconds to wake, fixed that with Uptime Robot. Two origins also mean configuring CORS.

Content

Markdown, with my own editor

Why: posts are full of code, and Markdown renders safely without storing HTML. No dangerouslySetInnerHTML anywhere. The editor has Edit, Split and Preview modes.

Trade-off: no image uploads yet, only external image URLs.

From commit to production

.github/workflows/ci.yml · on every push
  1. npm ci
  2. tsc server
  3. tsc -b client
  4. vitest server
  5. vitest client
  6. ✓ green check

Deployment is separate. On every push to master, Vercel builds the client and Render builds and restarts the API.

Not there yet: a red CI run doesn't block a deploy, because branch protection isn't enforced on a free private repo. Next steps are deploying only after tests pass, and adding lint and a production build to the pipeline.

Hard problems

Five 401s at once

When the access token expires, several requests fail together. Without care, each one would start its own refresh and they would revoke each other's tokens. An isRefreshing flag and a queue make sure only one refresh runs.

A bug hiding for two months

The API returned { data: { user } }, but the client read response.user. A page reload looked like a logout. It's the bug that made me want shared types.

Deep links that 404'd

Right after launch, opening any page other than the home page directly returned a 404. The fix was an SPA rewrite rule, so every route serves the app and React Router takes over.

CI that failed on day one

One step used a tsc flag that doesn't exist. Then Vitest found compiled copies of the tests in dist/ and ran them twice. Both are fixed now.

By the numbers

51commits
4.5months, idea to CI
12API endpoints
3data models
~4.8klines of TypeScript
  1. Nov 2025Wrote the PRD and picked the stack. Named it DevQuill.
  2. Dec 2025Monorepo, models, JWT login, post CRUD API, error handling.
  3. Jan 2026React client MVP. Live on Jan 28. Pinned posts, refresh tokens and a home page redesign in the days after.
  4. Feb 2026Markdown editor with live preview, share buttons.
  5. Mar–Apr 2026GitHub Actions CI and the first tests. Fixed the auth response bug.

What I'd do next

Security
Delivery
  • Deploy only after CI passes
  • Tests for the auth flow and post CRUD
  • Friendly handling of Render cold starts
Readers
  • Pagination beyond 10 posts
  • Per-post link previews for LinkedIn and X
  • A featured-posts endpoint for this portfolio
See it live

You're welcome to read the blog. The repository is private for now, and I'm happy to walk you through the code in an interview.

Like what you see?

I'm looking for my first role as a junior full-stack or backend developer, remote or in Belgrade. Let's talk.