Back to Blog
Configuration
December 15, 2024
5 min read

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 variables

    export 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

    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" \ -v

    Test 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

  • [ ] Add all your app domains to allowed origins
  • [ ] Include localhost URLs for development
  • [ ] Configure allowed methods (GET, POST, etc.)
  • [ ] Set up allowed headers if using custom headers
  • [ ] Enable credentials only if needed
  • [ ] Set appropriate max age for preflight caching
  • [ ] Test in all environments (dev, staging, prod)
  • [ ] Monitor CORS errors in production
  • Troubleshooting Steps

  • Check browser console for specific CORS error
  • Verify origin is in allowed list
  • Check if preflight request succeeds
  • Confirm headers match configuration
  • Test with a simple GET request first
  • Verify credentials setting matches needs
  • Conclusion

    CORS configuration doesn't have to be a roadblock in your SPA development. With Conduit Link, you can:

  • Configure CORS once and forget about it
  • Support multiple environments easily
  • Maintain security while enabling functionality
  • Debug issues quickly with clear error messages

Ready to simplify your SPA's API integration? Configure CORS in Conduit Link →

Share this article

View all articles

Ready to Secure Your APIs?

Join thousands of developers who trust Conduit Link to protect their API keys and build secure applications.

Connect Securely. Ship Faster.

© 2025 Conduit Link. All rights reserved.