Understanding CORS with JavaScript and Express

A comprehensive guide to Cross-Origin Resource Sharing

Introduction to CORS

Imagine you're running a restaurant (your web server). By default, you only serve people sitting inside your restaurant (requests from the same origin). But what if someone from the building next door (different origin) wants to order food? That's where CORS comes in - it's like creating a delivery policy that specifies which neighboring buildings you'll deliver to.

CORS (Cross-Origin Resource Sharing) is a security feature implemented by web browsers that controls how web pages in one domain can request and interact with resources from another domain. Without proper CORS configuration, your frontend JavaScript code running on mydomain.com cannot make API requests to api.otherdomain.com.

Project Setup

Let's create a practical example with both a frontend and backend service. Here's our project structure:

cors_demo/
├── frontend/
│   ├── index.html
│   └── app.js
└── backend/
    ├── package.json
    └── server.js

Backend Setup

First, create the backend directory and initialize your Node.js project:

mkdir cors_demo
cd cors_demo
mkdir backend
cd backend
npm init -y
npm install express cors

Create server.js in your backend folder:

// backend/server.js
const express = require('express');
const cors = require('cors');
const app = express();

// Basic CORS setup
app.use(cors({
    origin: 'http://localhost:5500' // Your frontend origin
}));

app.get('/api/data', (req, res) => {
    res.json({
        message: 'Hello from the backend!',
        timestamp: new Date()
    });
});

app.listen(3000, () => {
    console.log('Backend server running on http://localhost:3000');
});

This is like setting up your restaurant's delivery policy. The cors() middleware is your restaurant's delivery rules, and origin: 'http://localhost:5500' is saying "We only deliver to this specific address."

Frontend Implementation

Create your frontend files:

cd ../
mkdir frontend
cd frontend
touch index.html app.js

Add this content to index.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>CORS Demo</title>
</head>
<body>
    <h1>CORS Demo</h1>
    <button id="fetchData">Fetch Data</button>
    <div id="result"></div>
    <script src="app.js"></script>
</body>
</html>

And this to app.js:

// frontend/app.js
document.getElementById('fetchData').addEventListener('click', async () => {
    try {
        const response = await fetch('http://localhost:3000/api/data');
        const data = await response.json();
        document.getElementById('result').textContent = 
            `Message: ${data.message}\nTimestamp: ${data.timestamp}`;
    } catch (error) {
        document.getElementById('result').textContent = 
            `Error: ${error.message}`;
    }
});

Understanding CORS Options

CORS configuration is like creating different delivery policies for your restaurant. Here are common options:

Allow Multiple Origins

app.use(cors({
    origin: ['http://localhost:5500', 'https://myapp.com']
}));

This is like saying "We'll deliver to these specific addresses only."

Allow All Origins

app.use(cors({
    origin: '*'
}));

This is like saying "We'll deliver anywhere!" (Not recommended for production)

Custom Origin Validation

app.use(cors({
    origin: function(origin, callback) {
        const allowedOrigins = ['http://localhost:5500', 'https://myapp.com'];
        if (!origin || allowedOrigins.indexOf(origin) !== -1) {
            callback(null, true);
        } else {
            callback(new Error('Not allowed by CORS'));
        }
    }
}));

This is like having a delivery manager check each order's address against an approved list.

Common CORS Headers

CORS headers are like the delivery instructions on your order:

Example with All Options

app.use(cors({
    origin: 'http://localhost:5500',
    methods: ['GET', 'POST', 'PUT', 'DELETE'],
    allowedHeaders: ['Content-Type', 'Authorization'],
    credentials: true,
    maxAge: 86400 // How long to cache CORS response
}));

Practical Exercise

Let's create a more complex example that demonstrates real-world CORS usage:

Task: Build a Protected API

Create a new file called protected_server.js in your backend folder:

// backend/protected_server.js
const express = require('express');
const cors = require('cors');
const app = express();

// Custom middleware to check API key
const checkApiKey = (req, res, next) => {
    const apiKey = req.headers['x-api-key'];
    if (apiKey === 'your-secret-key') {
        next();
    } else {
        res.status(401).json({ error: 'Invalid API key' });
    }
};

// CORS configuration for protected API
app.use(cors({
    origin: 'http://localhost:5500',
    allowedHeaders: ['Content-Type', 'x-api-key'],
    methods: ['GET', 'POST']
}));

// Protected route
app.get('/api/protected', checkApiKey, (req, res) => {
    res.json({
        message: 'Access granted to protected data',
        data: {
            id: 123,
            content: 'Sensitive information'
        }
    });
});

app.listen(3001, () => {
    console.log('Protected server running on http://localhost:3001');
});

Create a new frontend file called protected.js:

// frontend/protected.js
async function fetchProtectedData() {
    try {
        const response = await fetch('http://localhost:3001/api/protected', {
            headers: {
                'x-api-key': 'your-secret-key'
            }
        });
        
        if (!response.ok) {
            throw new Error('Access denied');
        }
        
        const data = await response.json();
        console.log('Protected data:', data);
    } catch (error) {
        console.error('Error:', error.message);
    }
}

Common CORS Errors and Solutions

Like a restaurant dealing with delivery issues, here are common CORS problems and their solutions:

No 'Access-Control-Allow-Origin' Header

This is like trying to deliver to an address that's not on your approved list. Solution:

// Add the correct origin to your CORS config
app.use(cors({
    origin: 'http://yourdomain.com'
}));

Method Not Allowed

This is like trying to place a type of order your restaurant doesn't accept. Solution:

app.use(cors({
    methods: ['GET', 'POST', 'PUT', 'DELETE']
}));

Headers Not Allowed

This is like trying to add special instructions that your delivery service doesn't support. Solution:

app.use(cors({
    allowedHeaders: ['Content-Type', 'Authorization', 'X-Custom-Header']
}));

Further Topics to Explore

To deepen your understanding of CORS and web security:

Real-World Applications

CORS is crucial in many modern web architectures: