10 July 2026
Why My Blog API Requires Login Even to Read
Every other read endpoint in dev-hub is public: GET /api/projects returns data to anyone, no token required. GET /api/posts is the one exception, and the exception is deliberate.
The asymmetry
A Project has no draft state — everything in that table is meant to be seen. A Post does: publishedAt: null is a real, common state, and its title and content might be half-written, embarrassing, or simply not ready. If GET /api/posts/[id] were public, anyone who could guess or enumerate an id — and cuid()s are not secret, just hard to guess — could read a draft before it's meant to exist.
Where the line actually sits
The fix isn't clever: requireAuth() at the top of both GET and POST in /api/posts/route.ts. The public /blog and /blog/[slug] pages never call this endpoint at all — they query Prisma directly through getPublishedPosts() and getPublishedPostBySlug(), which hard-code the publishedAt <= now() filter. Only /dashboard/blog calls the API, and only an authenticated session reaches /dashboard/blog in the first place.
The general shape of the decision
The rule that fell out of this: an authenticated-only surface and a public surface should not share a code path if one of them can return a draft. It would have been possible to keep GET /api/posts public and filter drafts out for anonymous callers, but that puts the security boundary inside a conditional instead of at the door — and a conditional is exactly the kind of thing a future edit forgets.