CORS Configuration for Single-Page Applications
Master CORS settings in Conduit Link to enable secure cross-origin requests from your React, Vue, or Angular apps.
Conduit Link Team
Technical Content Writers
CORS Configuration for Single-Page Applications
Cross-Origin Resource Sharing (CORS) is often the first hurdle developers face when building SPAs that consume APIs. With Conduit Link, configuring CORS correctly is straightforward, but understanding the nuances helps you build more secure applications.
Understanding CORS in the Context of SPAs
When your React app running on localhost:3000
tries to fetch data from an API, the browser blocks the request by default. This is CORS in action—a security feature that prevents malicious websites from accessing your APIs.
// This fails without proper CORS configuration
fetch('https://api.example.com/data')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('CORS error:', error));
Common CORS Errors and What They Mean
1. No 'Access-Control-Allow-Origin' Header
Access to fetch at 'https://api.example.com' from origin 'http://localhost:3000'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is
present on the requested resource.
What it means: The API isn't configured to accept requests from your domain.
2. Credentials Not Supported
Access to fetch at 'https://api.example.com' from origin 'http://localhost:3000'
has been blocked by CORS policy: The value of the 'Access-Control-Allow-Credentials'
header in the response is '' which must be 'true' when the request's credentials
mode is 'include'.
What it means: You're trying to send cookies or auth headers, but the API isn't configured to accept them.
3. Preflight Request Failed
Access to fetch at 'https://api.example.com' from origin 'http://localhost:3000'
has been blocked by CORS policy: Response to preflight request doesn't pass
access control check.
What it means: The browser sent an OPTIONS request first, and it was rejected.
Configuring CORS in Conduit Link
Basic Configuration
In your Conduit Link dashboard:
- Navigate to your conduit link settings
- Find the CORS section
- Add your allowed origins: - Development:
http://localhost:3000
- Staging: https://staging.yourapp.com
- Production: https://yourapp.com
Advanced Configuration Options
{
"cors": {
"allowed_origins": [
"http://localhost:3000",
"http://localhost:5173", // Vite
"https://yourapp.com",
"https://*.yourapp.com" // Wildcard subdomain
],
"allowed_methods": ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
"allowed_headers": ["Content-Type", "Authorization", "X-Requested-With"],
"exposed_headers": ["X-Total-Count", "X-Page-Number"],
"allow_credentials": true,
"max_age": 86400 // 24 hours
}
}
Framework-Specific Implementation
React
// src/api/client.js
const API_BASE = 'https://proxy.conduit.link/YOUR-LINK-ID';
const ACCESS_TOKEN = import.meta.env.VITE_CONDUIT_ACCESS_TOKEN; // Store in env variablesexport const apiClient = {
get: async (endpoint) => {
const response = await fetch(${API_BASE}${endpoint}
, {
method: 'GET',
headers: {
'X-Access-Token': ACCESS_TOKEN,
'Content-Type': 'application/json',
},
credentials: 'include', // Include cookies if needed
});
if (!response.ok) {
throw new Error(HTTP error! status: ${response.status}
);
}
return response.json();
},
post: async (endpoint, data) => {
const response = await fetch(${API_BASE}${endpoint}
, {
method: 'POST',
headers: {
'X-Access-Token': ACCESS_TOKEN,
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify(data),
});
if (!response.ok) {
throw new Error(HTTP error! status: ${response.status}
);
}
return response.json();
},
};
// Usage in component
function UserProfile() {
const [user, setUser] = useState(null);
useEffect(() => {
apiClient.get('/user/profile')
.then(setUser)
.catch(console.error);
}, []);
return
{user?.name};
}
Vue 3
// src/composables/useApi.js
import { ref } from 'vue';const API_BASE = 'https://proxy.conduit.link/YOUR-LINK-ID';
const ACCESS_TOKEN = import.meta.env.VITE_CONDUIT_ACCESS_TOKEN;
export function useApi() {
const loading = ref(false);
const error = ref(null);
const fetchData = async (endpoint, options = {}) => {
loading.value = true;
error.value = null;
try {
const response = await fetch(${API_BASE}${endpoint}
, {
...options,
headers: {
'X-Access-Token': ACCESS_TOKEN,
'Content-Type': 'application/json',
...options.headers,
},
credentials: 'include',
});
if (!response.ok) {
throw new Error(HTTP error! status: ${response.status}
);
}
return await response.json();
} catch (err) {
error.value = err.message;
throw err;
} finally {
loading.value = false;
}
};
return {
loading,
error,
get: (endpoint) => fetchData(endpoint, { method: 'GET' }),
post: (endpoint, data) => fetchData(endpoint, {
method: 'POST',
body: JSON.stringify(data),
}),
};
}
// Usage in component
Loading...
Error: {{ error }}
{{ userData?.name }}
Angular
typescript
// src/app/services/api.service.ts
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Observable } from 'rxjs';
import { environment } from '@/environments/environment';@Injectable({
providedIn: 'root'
})
export class ApiService {
private apiBase = 'https://proxy.conduit.link/YOUR-LINK-ID';
private accessToken = environment.conduitAccessToken;
constructor(private http: HttpClient) {}
private getHeaders(): HttpHeaders {
return new HttpHeaders({
'X-Access-Token': this.accessToken,
'Content-Type': 'application/json',
});
}
get(endpoint: string): Observable {
return this.http.get(
${this.apiBase}${endpoint}
,
{
headers: this.getHeaders(),
withCredentials: true
}
);
}
post(endpoint: string, data: any): Observable {
return this.http.post(
${this.apiBase}${endpoint}
,
data,
{
headers: this.getHeaders(),
withCredentials: true
}
);
}
}
// Usage in component
@Component({
selector: 'app-user-profile',
template:
Loading...
Error: {{ error }}
{{ user.name }}
})
export class UserProfileComponent implements OnInit {
user: User | null = null;
loading = true;
error: string | null = null;
constructor(private apiService: ApiService) {}
ngOnInit() {
this.apiService.get('/user/profile').subscribe({
next: (data) => {
this.user = data;
this.loading = false;
},
error: (err) => {
this.error = err.message;
this.loading = false;
}
});
}
}
Security Best Practices
1. Never Use Wildcard Origins in Production
// ❌ Don't do this in production
{
"allowed_origins": ["*"]
}// ✅ Do this instead
{
"allowed_origins": [
"https://yourapp.com",
"https://www.yourapp.com"
]
}
2. Restrict Methods to What You Need
// ❌ Allowing all methods when you only need GET
{
"allowed_methods": ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"]
}// ✅ Only allow what you use
{
"allowed_methods": ["GET", "POST", "OPTIONS"]
}
3. Be Careful with Credentials
// Only include credentials when necessary
fetch(url, {
credentials: 'include' // Sends cookies
});// For public APIs, omit credentials
fetch(url, {
credentials: 'omit' // More secure for public data
});
Handling Development vs Production
Environment-Based Configuration
// src/config/api.js
const config = {
development: {
apiBase: 'https://proxy.conduit.link/YOUR-DEV-LINK-ID',
credentials: 'include',
},
production: {
apiBase: 'https://proxy.conduit.link/YOUR-PROD-LINK-ID',
credentials: 'same-origin',
},
};export default config[process.env.NODE_ENV || 'development'];
Using Environment Variables
// .env.development
VITE_API_BASE=https://proxy.conduit.link/YOUR-DEV-LINK-ID
VITE_CONDUIT_ACCESS_TOKEN=cl_dev_xxxxxxxxxxxxx
VITE_API_CREDENTIALS=include// .env.production
VITE_API_BASE=https://proxy.conduit.link/YOUR-PROD-LINK-ID
VITE_CONDUIT_ACCESS_TOKEN=cl_prod_xxxxxxxxxxxxx
VITE_API_CREDENTIALS=same-origin
// src/api/client.js
const apiBase = import.meta.env.VITE_API_BASE;
const accessToken = import.meta.env.VITE_CONDUIT_ACCESS_TOKEN;
const credentials = import.meta.env.VITE_API_CREDENTIALS;
Debugging CORS Issues
1. Check Preflight Requests
Open your browser's Network tab and look for OPTIONS requests:
// Add logging to debug CORS
fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
})
.then(response => {
console.log('Response headers:', response.headers);
return response.json();
})
.catch(error => {
console.error('CORS Error:', error);
});
2. Verify Response Headers
The response should include these headers:
Access-Control-Allow-Origin: https://yourapp.com
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
3. Test with cURL
Test CORS preflight
curl -X OPTIONS https://proxy.conduit.link/YOUR-LINK-ID/api/data \
-H "Origin: https://yourapp.com" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: Content-Type, X-Access-Token" \
-vTest actual request
curl -X POST https://proxy.conduit.link/YOUR-LINK-ID/api/data \
-H "Origin: https://yourapp.com" \
-H "X-Access-Token: YOUR_CONDUIT_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"test": "data"}' \
-v
Common Patterns and Solutions
SPA with Authentication
// Authenticated requests with Conduit Link
const authenticatedFetch = async (url, options = {}) => {
const accessToken = 'YOUR_CONDUIT_ACCESS_TOKEN'; // Your Conduit Link access token
const userToken = localStorage.getItem('auth_token'); // Optional: User JWT return fetch(url, {
...options,
headers: {
...options.headers,
'X-Access-Token': accessToken, // Required for Conduit Link
'Authorization': userToken ? Bearer ${userToken}
: undefined, // Optional: for JWT verification
'Content-Type': 'application/json',
},
});
};
File Uploads from SPA
// Handling file uploads with proper CORS
const uploadFile = async (file) => {
const formData = new FormData();
formData.append('file', file); const response = await fetch('https://proxy.conduit.link/YOUR-LINK-ID/upload', {
method: 'POST',
headers: {
'X-Access-Token': 'YOUR_CONDUIT_ACCESS_TOKEN', // Required
// Don't set Content-Type for FormData, let browser set boundary
},
body: formData,
credentials: 'include',
});
return response.json();
};
Streaming Responses
// Server-sent events with CORS
const eventSource = new EventSource(
'https://proxy.conduit.link/YOUR-LINK-ID/stream',
{ withCredentials: true }
);eventSource.onmessage = (event) => {
console.log('New message:', event.data);
};
Quick Reference
Conduit Link CORS Checklist
Troubleshooting Steps
Conclusion
CORS configuration doesn't have to be a roadblock in your SPA development. With Conduit Link, you can:
Ready to simplify your SPA's API integration? Configure CORS in Conduit Link →
Ready to Secure Your APIs?
Join thousands of developers who trust Conduit Link to protect their API keys and build secure applications.