Taming CORS in a Microservices Monorepo: From Async Validation to a Unified Config
By Muhammad Sharjeel | Published on Sun May 11 2025

What is CORS?
Cross-Origin Resource Sharing (CORS) is a security feature implemented by web browsers. It restricts web applications running on one origin (domain) from interacting with resources from another unless explicitly allowed.
Let’s say your frontend is hosted at https://app.example.com
and your API is at https://api.example.com
. By default, browsers will block requests from the frontend to the backend unless the backend server allows it via CORS headers.
Why CORS is Necessary
CORS exists to protect users from malicious websites that attempt to read sensitive data from another origin (like your banking session). Without CORS, any site could fetch data from any API the user is authenticated with — a nightmare for security.
In development, however, it often feels like a headache: “Why is my API call failing even though the backend is up?” More often than not, it’s a CORS misconfiguration.
How CORS Works (Including Preflight)
There are two types of CORS requests:
- Simple Requests: Like a
GET
with no custom headers. - Preflighted Requests: Any request that modifies data (e.g.,
POST
,PUT
) or includes custom headers triggers a preflight check — an initialOPTIONS
request sent by the browser to verify if the real request is safe to send.
If your server fails to respond to this OPTIONS
request correctly, the real request is never made.
My CORS Problem: Microservices in a Monorepo
I’m working on a platform called PolyX — built using a microservices architecture, all managed in a Yarn monorepo. Each service runs independently but shares the same workspace — and that’s where the challenge came in.
I initially allowed each service to define its own list of allowed CORS origins. But in a monorepo setup, this quickly became unmanageable. A new frontend domain? I had to update every service manually. Worse: discrepancies started appearing between environments.
Early Attempt: Asynchronous Origin Validation
My first idea was to validate origins asynchronously — fetching allowed origins from a central store (like Redis or MongoDB) during every request. It was technically flexible, but practically inefficient.
Why? Because:
- Every request triggered a DB read
- This added latency to every API call
- Even OPTIONS (preflight) requests had to go through this check
It was elegant in theory, but a performance and stability bottleneck in practice.
The Real Solution: A Shared JSON File
So, I went back to basics. I created a single JSON file inside the monorepo:
/poly-auth/poly-common/config/allowed-origins.json
Every microservice now loads its allowed origins from this file at startup. And this is where the magic of a monorepo shines — since everything lives in one place, all services can import and reference the same file easily.
Sample JSON File
{
"allowedOrigins": [
"https://admin.example.com",
"https://dashboard.example.com",
"https://app.example.com"
]
}
Why a Single Source of Truth Matters
Beyond CORS, this experience taught me the value of single source of truth (SSOT) in monorepo architecture:
- ✅ Reduces config drift across services
- ✅ Makes onboarding easier
- ✅ Enables automation and CI checks on shared settings
- ✅ Avoids bugs caused by inconsistent behavior
This pattern now powers not just CORS — but also shared error codes, status enums, role definitions, and even environment flags.
Implementing It in Node.js
Here’s a basic snippet of how a service loads and uses the JSON config:
import fs from 'fs';
import path from 'path';
const ORIGIN_FILE_PATH = path.join(__dirname, '../config/allowed-origins.json');
const rawOrigins = fs.readFileSync(ORIGIN_FILE_PATH, 'utf-8');
const { allowedOrigins } = JSON.parse(rawOrigins);
app.use(cors({
origin: (origin, callback) => {
if (!origin || allowedOrigins.includes(origin)) {
return callback(null, true);
}
return callback(new Error("Not allowed by CORS"));
},
credentials: true
}));
This way, no matter which service handles a request, it follows the same rules — with zero runtime calls to databases or external services.
Lessons Learned
Here’s what I learned through this journey:
- Don’t over-engineer early. CORS doesn’t need to be dynamic for most setups.
- Monorepos simplify shared config. Lean into that instead of duplicating.
- Performance matters in security middleware. Especially for preflight requests.
Final Thoughts
CORS errors can feel like black magic at times — but understanding how they work and structuring your architecture with clarity makes all the difference. For me, it wasn’t about a fancy fix. It was about designing with simplicity, maintainability, and shared ownership in mind.
And sometimes, the best solution is the one that lives in a good old JSON file.