BorisovAI
All posts
Learningnotes-serverCursor IDE

Debugging a Monorepo: When Your API Returns HTML Instead of JSON

Debugging a Monorepo: When Your API Returns HTML Instead of JSON

I was handed a monorepo mystery. Notes Server—a sophisticated multi-package project with a backend API, Vue.js web client, embeddings service, CLI tools, and even a Telegram bot—was running, but the /api/notes endpoint was returning a cryptic 404 wrapped in HTML instead of JSON.

The project structure looked solid: npm workspaces, Vite dev server on port 5173 proxying requests to an Express backend on port 3000. Everything should work. But when I hit http://localhost:3000/api/notes, the server responded with 53KB of HTML. That’s never a good sign.

The culprit? Route registration order matters. In Express, middleware and routes are matched in the order they’re registered. The backend had two layers: first, createApp() from app.ts registered the API routes (/api/notes, /api/thoughts, etc.), then index.ts added static file serving and a catch-all root route. The static middleware was accidentally catching requests before they reached the API handlers. Classic Express gotcha—a / route or express.static() handler placed too early in the stack will swallow everything.

I verified the routing logic by inspecting both files. The routes were definitely there in notes-routes.ts. The middleware chain was the problem. The fix? Ensure API routes are registered before any static or catch-all handlers. This is especially tricky in monorepos where multiple entry points can conflict.

What made debugging harder was the Windows environment. I couldn’t just curl the endpoint from Git Bash to inspect headers—curl on Windows corrupts UTF-8 in request bodies, so I switched to PowerShell’s Invoke-WebRequest for clean HTTP testing. It’s a sneaky platform quirk that catches a lot of developers off guard.

The web client itself was fine. Vite’s proxy configuration was correctly forwarding API calls to localhost:3000, and Vue was loading without errors. The problem was purely backend routing.

Here’s the tech fact: Monorepos introduce hidden coupling. When you have six packages sharing dependencies and entry points, the order of operations becomes critical. A stray app.use(express.static()) in one file can silently break API contracts in another, and the error manifests as your frontend receiving HTML instead of JSON—which browsers happily display as a blank page or cryptic error.

The lesson: always test your routes independently before assuming the frontend integration is the problem. A quick curl (or Invoke-WebRequest on Windows) to each endpoint takes 30 seconds and saves 30 minutes of debugging.


Why did the database administrator leave his wife? She had one-to-many relationships. 😄

Metadata

Session ID:
grouped_notes-server_20260225_1115
Dev Joke
Почему Prometheus считает себя лучше всех? Потому что Stack Overflow так сказал

Rate this content

0/1000