Introduction

CORS (Cross-Origin Resource Sharing) errors occur when a frontend application running on localhost:3000 tries to fetch data from an API on a different origin like localhost:8080 or api.example.com. The browser blocks the request and logs:

bash
Access to fetch at 'http://localhost:8080/api/data' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

This is the most common error developers encounter when building frontend applications that communicate with separate API backends.

Symptoms

  • Browser console shows "Access has been blocked by CORS policy" error
  • Fetch or XMLHttpRequest returns a network error despite the API working in Postman
  • Preflight OPTIONS request returns 403 or 405
  • The API works when called directly from the browser address bar
  • Error only occurs in the browser; curl and API testing tools work fine

Common Causes

  • Different ports (localhost:3000 vs localhost:8080) count as different origins
  • API server does not include Access-Control-Allow-Origin response header
  • Preflight OPTIONS request fails because the API does not handle it
  • Credentials mode (withCredentials: true) requires Access-Control-Allow-Credentials: true
  • Custom request headers (Authorization, Content-Type: application/json) trigger preflight requests

Step-by-Step Fix

  1. 1.Configure CORS headers on the API server. For Express.js:
  2. 2.```javascript
  3. 3.const cors = require('cors');
  4. 4.const app = express();

app.use(cors({ origin: 'http://localhost:3000', credentials: true, methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], allowedHeaders: ['Content-Type', 'Authorization'], })); ```

  1. 1.Handle preflight OPTIONS requests if not using a CORS middleware:
  2. 2.```javascript
  3. 3.app.options('/api/*', (req, res) => {
  4. 4.res.header('Access-Control-Allow-Origin', 'http://localhost:3000');
  5. 5.res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
  6. 6.res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
  7. 7.res.header('Access-Control-Allow-Credentials', 'true');
  8. 8.res.sendStatus(204);
  9. 9.});
  10. 10.`
  11. 11.Use a development proxy in your frontend build tool instead of configuring the API. For Vite (vite.config.ts):
  12. 12.```typescript
  13. 13.export default defineConfig({
  14. 14.server: {
  15. 15.proxy: {
  16. 16.'/api': {
  17. 17.target: 'http://localhost:8080',
  18. 18.changeOrigin: true,
  19. 19.},
  20. 20.},
  21. 21.},
  22. 22.});
  23. 23.`
  24. 24.This makes the browser think it is talking to the same origin, avoiding CORS entirely.
  25. 25.For Create React App, add to package.json:
  26. 26.```json
  27. 27.{
  28. 28."proxy": "http://localhost:8080"
  29. 29.}
  30. 30.`
  31. 31.For Nginx reverse proxy in development:
  32. 32.```nginx
  33. 33.server {
  34. 34.listen 80;
  35. 35.server_name localhost;

location / { proxy_pass http://localhost:3000; }

location /api/ { proxy_pass http://localhost:8080; } } ```

  1. 1.Verify the fix by checking the response headers in the browser's Network tab:
  2. 2.`
  3. 3.Access-Control-Allow-Origin: http://localhost:3000
  4. 4.Access-Control-Allow-Credentials: true
  5. 5.Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
  6. 6.`

Prevention

  • Use a development proxy configuration to eliminate CORS issues during local development
  • Document the expected CORS headers in your API's OpenAPI/Swagger specification
  • Test API endpoints with browser-based fetch, not just curl or Postman, during development
  • Configure CORS with specific allowed origins, never use * in production with credentials
  • Add CORS integration tests to your API test suite that verify headers for all endpoints
  • Use environment-specific CORS configuration: development allows all localhost origins, production restricts to specific domains