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:
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-Originresponse header - Preflight OPTIONS request fails because the API does not handle it
- Credentials mode (
withCredentials: true) requiresAccess-Control-Allow-Credentials: true - Custom request headers (Authorization, Content-Type: application/json) trigger preflight requests
Step-by-Step Fix
- 1.Configure CORS headers on the API server. For Express.js:
- 2.```javascript
- 3.const cors = require('cors');
- 4.const app = express();
app.use(cors({ origin: 'http://localhost:3000', credentials: true, methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], allowedHeaders: ['Content-Type', 'Authorization'], })); ```
- 1.Handle preflight OPTIONS requests if not using a CORS middleware:
- 2.```javascript
- 3.app.options('/api/*', (req, res) => {
- 4.res.header('Access-Control-Allow-Origin', 'http://localhost:3000');
- 5.res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
- 6.res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
- 7.res.header('Access-Control-Allow-Credentials', 'true');
- 8.res.sendStatus(204);
- 9.});
- 10.
` - 11.Use a development proxy in your frontend build tool instead of configuring the API. For Vite (
vite.config.ts): - 12.```typescript
- 13.export default defineConfig({
- 14.server: {
- 15.proxy: {
- 16.'/api': {
- 17.target: 'http://localhost:8080',
- 18.changeOrigin: true,
- 19.},
- 20.},
- 21.},
- 22.});
- 23.
` - 24.This makes the browser think it is talking to the same origin, avoiding CORS entirely.
- 25.For Create React App, add to
package.json: - 26.```json
- 27.{
- 28."proxy": "http://localhost:8080"
- 29.}
- 30.
` - 31.For Nginx reverse proxy in development:
- 32.```nginx
- 33.server {
- 34.listen 80;
- 35.server_name localhost;
location / { proxy_pass http://localhost:3000; }
location /api/ { proxy_pass http://localhost:8080; } } ```
- 1.Verify the fix by checking the response headers in the browser's Network tab:
- 2.
` - 3.Access-Control-Allow-Origin: http://localhost:3000
- 4.Access-Control-Allow-Credentials: true
- 5.Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
- 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