CORS in Deno and Bun feels similar at first because both runtimes lean hard into the Web Platform. You get Request, Response, Headers, and fetch, so the mechanics are familiar. The difference shows up when you actually wire policies into a real server, especially once preflight requests, credentials, and route-level behavior enter the picture.
My short take: Deno feels more explicit and standards-first. Bun feels faster to get running and very ergonomic, but you need to be just as disciplined about the policy because neither runtime magically saves you from bad CORS decisions.
The baseline: CORS is headers, not magic
No runtime “does CORS” for you. The browser enforces it based on response headers. Your server just emits the right ones.
At minimum, you’ll usually deal with:
Access-Control-Allow-OriginAccess-Control-Allow-MethodsAccess-Control-Allow-HeadersAccess-Control-Allow-CredentialsAccess-Control-Expose-HeadersAccess-Control-Max-Age
And for preflight, the browser sends:
OriginAccess-Control-Request-MethodAccess-Control-Request-Headers
A real-world example helps here. api.github.com sends:
access-control-allow-origin: *
access-control-expose-headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset
That’s a good reminder that CORS is often not just “allow origin.” Exposed headers matter when frontend code needs rate limit data, pagination links, or cache metadata.
Deno: clean, explicit, standards-shaped
Deno’s native HTTP APIs make CORS implementation feel very direct. You can do everything with Deno.serve and standard Response objects.
Simple Deno CORS handler
const ALLOWED_ORIGINS = new Set([
"https://app.example.com",
"https://admin.example.com",
]);
function buildCorsHeaders(origin: string | null) {
const headers = new Headers();
if (origin && ALLOWED_ORIGINS.has(origin)) {
headers.set("Access-Control-Allow-Origin", origin);
headers.set("Vary", "Origin");
headers.set("Access-Control-Allow-Credentials", "true");
headers.set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
headers.set("Access-Control-Allow-Headers", "Content-Type, Authorization");
headers.set("Access-Control-Max-Age", "600");
headers.set("Access-Control-Expose-Headers", "ETag, Link, X-RateLimit-Remaining");
}
return headers;
}
Deno.serve((req) => {
const origin = req.headers.get("Origin");
const corsHeaders = buildCorsHeaders(origin);
if (req.method === "OPTIONS") {
return new Response(null, {
status: 204,
headers: corsHeaders,
});
}
const headers = new Headers(corsHeaders);
headers.set("Content-Type", "application/json");
return new Response(JSON.stringify({ ok: true }), {
status: 200,
headers,
});
});
This is Deno at its best: small surface area, no framework required, very little abstraction.
Pros of CORS in Deno
1. Standards-first APIs
If you know the Fetch API, you already know most of what matters. I like that there’s not much hidden behavior.
2. Easy to reason about preflight logic
With Deno.serve, handling OPTIONS is dead simple. That matters because preflight bugs are usually boring, repetitive mistakes.
3. Good fit for edge-style services
Deno’s model works nicely when you want a thin HTTP service with explicit security controls.
4. Minimal framework dependency
You can implement a solid policy without pulling in middleware just to set six headers.
Cons of CORS in Deno
1. You own the policy details
This is also the biggest downside. If you forget Vary: Origin, or return * alongside credentials, Deno won’t stop you.
2. Middleware story depends on your stack
If you use Oak, Hono, Fresh, or another framework on top of Deno, CORS handling can vary a lot. The runtime itself is clean, but the app-level story changes.
3. Easy to be too manual
I’ve seen teams copy-paste CORS logic into multiple handlers and drift into inconsistent behavior.
Bun: fast server startup, familiar APIs, same CORS footguns
Bun’s Bun.serve is similarly straightforward. If you’ve worked with modern Web APIs, you’ll feel at home.
Simple Bun CORS handler
const allowedOrigins = new Set([
"https://app.example.com",
"https://admin.example.com",
]);
function corsHeaders(origin: string | null) {
const headers = new Headers();
if (origin && allowedOrigins.has(origin)) {
headers.set("Access-Control-Allow-Origin", origin);
headers.set("Vary", "Origin");
headers.set("Access-Control-Allow-Credentials", "true");
headers.set("Access-Control-Allow-Methods", "GET, POST, PATCH, DELETE, OPTIONS");
headers.set("Access-Control-Allow-Headers", "Content-Type, Authorization");
headers.set("Access-Control-Max-Age", "86400");
headers.set("Access-Control-Expose-Headers", "ETag, Link, X-RateLimit-Remaining");
}
return headers;
}
Bun.serve({
port: 3000,
fetch(req) {
const origin = req.headers.get("Origin");
const headers = corsHeaders(origin);
if (req.method === "OPTIONS") {
return new Response(null, {
status: 204,
headers,
});
}
headers.set("Content-Type", "application/json");
return new Response(JSON.stringify({ ok: true }), {
headers,
});
},
});
Bun is just as capable here. The browser doesn’t care whether the response came from Deno, Bun, Node, Go, or nginx. It only cares whether the headers are valid.
Pros of CORS in Bun
1. Very quick to ship simple APIs
Bun.serve is ergonomic. For small services, CORS setup is painless.
2. Familiar Web API model
Like Deno, Bun keeps things close to standards. That’s good for portability and developer sanity.
3. Good performance characteristics
CORS itself is not performance-heavy, but Bun’s fast startup and request handling make it attractive for lightweight APIs.
4. Easy migration path for JS developers
If your team already thinks in terms of fetch, Request, and Response, Bun doesn’t fight that mental model.
Cons of CORS in Bun
1. Same sharp edges as every manual CORS setup
You can still misconfigure credentials, forget OPTIONS, or over-allow headers.
2. Ecosystem consistency is still a factor
Depending on framework and middleware choice, CORS behavior can become less obvious than the raw runtime API suggests.
3. Fast iteration can encourage sloppy policy
This is more cultural than technical, but I’ve noticed teams moving quickly with Bun sometimes treat CORS as “just make the frontend work,” which is exactly how bad policies survive.
Head-to-head: where they differ
Deno wins on explicitness
Deno feels a bit more “this is the platform, build carefully.” I prefer it when I want the security policy to be obvious in code review. There’s less temptation to hide header behavior behind layers.
Bun wins on developer speed
Bun is very good when you want to stand up an API quickly. If your team values fast local iteration and a very lightweight setup, Bun is appealing.
They tie on actual CORS capability
Neither runtime has a structural advantage in how CORS works. They both expose the primitives you need. The quality of your implementation matters more than the runtime.
The credential trap
This is where people still mess up production deployments.
If you need cookies or HTTP auth in browser requests:
- you must set
Access-Control-Allow-Credentials: true - you cannot use
Access-Control-Allow-Origin: * - you should reflect only trusted origins
- you should send
Vary: Origin
Bad:
headers.set("Access-Control-Allow-Origin", "*");
headers.set("Access-Control-Allow-Credentials", "true");
That combination is invalid for credentialed browser requests.
Good:
if (origin === "https://app.example.com") {
headers.set("Access-Control-Allow-Origin", origin);
headers.set("Access-Control-Allow-Credentials", "true");
headers.set("Vary", "Origin");
}
If you don’t need credentials, wildcard origin is often fine for public APIs. GitHub does exactly that with access-control-allow-origin: *, while still exposing useful response headers.
Exposed headers are underrated
A lot of teams stop at Allow-Origin and forget that frontend code can’t read arbitrary response headers unless you expose them.
If your SPA needs pagination, caching, or rate-limit data, expose those headers deliberately.
Example:
headers.set(
"Access-Control-Expose-Headers",
"ETag, Link, Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset"
);
That pattern is common in serious APIs. GitHub’s exposed header list is a solid real-world model because it includes operational metadata clients actually use.
Practical advice for both runtimes
Prefer allowlists over reflection
Don’t blindly mirror any Origin header back to the client. Check it against a known set.
Handle OPTIONS early
Preflight requests should be cheap and boring. Return 204 No Content with the right headers and move on.
Use Vary: Origin
If your response changes based on the request origin, caches need to know that.
Keep CORS policy centralized
Write one helper and reuse it. Don’t scatter origin logic across routes unless you truly need route-specific rules.
Don’t confuse CORS with auth
CORS controls which browser contexts can read responses. It is not authentication and it is not CSRF protection. If you’re working on broader header hardening too, that’s a separate conversation; for non-CORS headers, I usually point people to CSP Guide.
My recommendation
Pick Deno if you want a very explicit, standards-aligned server layer and you care about keeping security behavior easy to audit.
Pick Bun if you want a smooth developer experience, quick startup, and a simple path to shipping APIs fast.
For CORS alone, I wouldn’t choose one over the other. The browser is the real enforcer, and both runtimes give you the same core tools. What matters is whether you write a strict policy, handle preflight properly, and expose only the headers your frontend genuinely needs.
Official docs: