A small Gin API project to learn CORS (Cross-Origin Resource Sharing) — why browsers enforce it, how preflight works, and how to configure it in Go.
- Backend (Gin): REST endpoints on
http://localhost:8080 - Frontend (HTML):
test.htmlserved onhttp://localhost:3000 - CORS middleware:
middleware/cors.go— allows cross-origin browser requests from the frontend dev server
Browser (localhost:3000) → API (localhost:8080)
different ports = different origins = CORS applies
go-crud/
├── main.go # Boot: router, middleware, Run()
├── middleware/cors.go # CORS headers + OPTIONS preflight
├── handler/user.go # Request handlers
├── routes/routes.go # URL → handler wiring
├── models/user.go # Data structs
└── test.html # Browser UI to test cross-origin calls
Terminal 1 — API
go run .
# or
CompileDaemon -command="./go-crud"Terminal 2 — Frontend
python3 -m http.server 3000Open http://localhost:3000/test.html → click Create / Delete → check DevTools → Network tab.
CORS is a browser security policy. It stops malicious websites from using your browser session to call other sites on your behalf.
You're logged into bank.com (session cookie in browser).
You visit evil-site.com. Without CORS, evil-site could run:
fetch("https://bank.com/api/transfer", {
method: "POST",
body: JSON.stringify({ to: "attacker", amount: 10000 }),
credentials: "include" // sends your bank cookie!
})CORS blocks this — bank.com never sends Access-Control-Allow-Origin: https://evil-site.com, so the browser refuses to expose the response (and often blocks the request entirely).
CORS protects users. Your API middleware tells the browser: "Only these frontends may call me from JavaScript."
Browser blocks the request. Console error:
Access to fetch at 'http://localhost:8080/delete' from origin 'http://localhost:3000'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present
Network tab shows:
- OPTIONS
/create→ 404 or missing CORS headers (preflight fails) - POST
/create→ failed
Preflight passes, then the real request goes through.
Network tab shows two requests on button click:
| # | Method | URL | Status | Purpose |
|---|---|---|---|---|
| 1 | OPTIONS | /create |
204 | Preflight — browser asks permission |
| 2 | POST | /create |
201 | Real request — only after preflight passes |
OPTIONS preflight request headers (browser sends):
Origin: http://localhost:3000
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type
OPTIONS response headers (API returns):
Access-Control-Allow-Origin: http://localhost:3000
Access-Control-Allow-Methods: GET,POST,PUT,DELETE,PATCH,HEAD,OPTIONS
Access-Control-Allow-Headers: Content-Type
| Header | Purpose |
|---|---|
Access-Control-Allow-Origin |
Which frontend origins may call this API |
Access-Control-Allow-Methods |
Which HTTP methods are allowed (PUT, DELETE, PATCH must be listed) |
Access-Control-Allow-Headers |
Which request headers the browser may send (e.g. Content-Type) |
Access-Control-Allow-Credentials |
Whether cookies/auth headers are allowed (true / omit) |
Note: GET, POST, HEAD are CORS-safelisted methods — browsers always allow them in preflight. PUT, DELETE, PATCH must appear in
Allow-Methods.
Access to fetch at 'http://localhost:8080/delete' from origin 'http://localhost:3000'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present
as DELETE is not in the Access-Control-Allow-Methods
Network tab shows:
- OPTIONS
/delete→ missing CORS method (preflight fails) - POST
/delete→ failed
Browser preflight hits the same URL as the real request (e.g. OPTIONS /create, not /options).
router.OPTIONS("/options")→ only handles that one pathrouter.Use(middleware.CORS())→ handles all paths + attaches CORS headers to every response
This is the standard production pattern.
Current code (middleware/cors.go):
c.Header("Access-Control-Allow-Origin", "http://localhost:3000")Production pattern — whitelist specific origins:
// Dev
"http://localhost:3000"
// Production
"https://app.google.com"
"https://admin.google.com"Real apps typically use:
- A list of allowed origins (dev + staging + prod)
- Env config:
ALLOWED_ORIGINS=https://app.example.com,https://staging.example.com
Never in production:
"*" // allows ANY website to call your API from a browser| Scenario | CORS needed? | Why |
|---|---|---|
| Same host (UI + API on same origin) | No | e.g. google.com UI → google.com/search/... |
| Server-to-server (K8s, Google Chrome → Google Search Nodes) | No | No browser involved — JWT/token auth instead |
| Postman / curl / backend scripts | No | CORS is browser-only |
Local dev (Angular :4200 → API :8080) |
Yes | Cross-origin — whitelist required |
| External plugin / third-party UI | Yes | Cross-origin — whitelist required |
Example ( prod config):
google_cors_origins = "" # empty = same-origin design, CORS off by default
# set only when something outside that host needs browser access
| Method | Route | Purpose |
|---|---|---|
| GET | /retrieve |
Read |
| POST | /create |
Create |
| PUT | /update |
Full replace |
| PATCH | /patch |
Partial update |
| DELETE | /delete |
Remove |
| HEAD | /head |
Metadata only (no body) |
| OPTIONS | (middleware) | Preflight (automatic) |
- Different port = different origin —
localhost:3000≠localhost:8080 - POST with JSON triggers preflight — browser sends OPTIONS first
- Preflight ≠ route exists — middleware returns 204 even if POST route is missing
- Network tab shows server response even when CORS blocks JS — check Console too
- Postman never shows preflight — only browsers do
- CORS is not auth — it only controls which origins can read responses in a browser
- enable-cors.org — Resources
- Okta — Enable CORS Guide
- martini-contrib/cors (Go reference)
- web.dev — CORS & Preflight requests
- MDN — CORS




