One API Gateway, Seven Apps: Why We Consolidated All Backend Routes
When VietProHub grew from one corporate site into seven Next.js apps sharing a single Firebase project, every app carried its own copy of API routes. Contact forms, newsletter signups, admin endpoints — the same logic duplicated, drift inevitable.
We replaced it all with one Cloud Function acting as an API gateway.
The problem with seven API bundles
Static export complicates things. When you deploy next export output to Firebase Hosting, there is no server to run /api/* routes. Each app that needed a backend had to either:
- Keep a server runtime (defeats static export), or
- Ship its own set of Cloud Functions.
Both options meant seven places to update rate limits, seven cold starts to warm, seven secrets to rotate.
One gateway, one routing table
The gateway is a single onRequest function behind a rewrite on every hosting site:
{
"rewrites": [
{ "source": "/api/**", "function": "apiGateway" }
]
}
Inside, a plain routing table dispatches by path and method:
if (path === '/api/contact' && method === 'POST') return handleContact(db, req, res)
if (path === '/api/giscus/counts' && method === 'GET') return handleGiscusCounts(req, res)
That's it. No framework, no router library — 1,200 lines of explicit, greppable handlers.
What we got
- One cold start to optimize, not seven
- One rate-limiting table — in-memory per instance, shared across all apps
- One place for CORS, validation, and audit logging
- Deploys that don't touch the frontend at all
The tradeoffs
An in-memory rate limiter resets per instance. Fine for our traffic; we'll move to Firestore-backed counters if abuse appears. And a monolith gateway means a bug affects every app at once — we mitigate with strict input validation and integration tests per route.
The lesson: when every app shares one backend, consolidating is not premature optimization. It's the default that should have shipped first.