Skip to main content
Cross-Origin Resource Sharing (CORS) is a security mechanism that allows your API to be accessed from web pages on different domains. Express Zod API provides built-in CORS configuration.

Enabling CORS

CORS is disabled by default. You must explicitly enable it in your configuration:
import { createConfig } from "express-zod-api";

const config = createConfig({
  http: { listen: 8090 },
  cors: true, // Enable CORS with default headers
});
CORS must be explicitly set to true or false. This ensures you make a conscious decision about cross-origin access to your API.

Default CORS Headers

When you set cors: true, the following headers are sent:
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization

Custom CORS Configuration

For more control, you can provide a function that returns custom CORS headers:
import { createConfig } from "express-zod-api";

const config = createConfig({
  cors: ({ defaultHeaders, request, endpoint, logger }) => ({
    ...defaultHeaders,
    "Access-Control-Allow-Origin": "https://example.com",
    "Access-Control-Max-Age": "5000",
    "Access-Control-Allow-Credentials": "true",
  }),
});

Configuration Parameters

The CORS function receives:
ParameterTypeDescription
defaultHeadersRecord<string, string>Default CORS headers
requestRequestExpress request object
endpointAbstractEndpointThe matched endpoint
loggerActualLoggerLogger instance

Common CORS Patterns

Allow Specific Origin

const config = createConfig({
  cors: ({ defaultHeaders }) => ({
    ...defaultHeaders,
    "Access-Control-Allow-Origin": "https://myapp.com",
  }),
});

Allow Multiple Origins

const allowedOrigins = [
  "https://app.example.com",
  "https://admin.example.com",
  "http://localhost:3000",
];

const config = createConfig({
  cors: ({ defaultHeaders, request }) => {
    const origin = request.headers.origin;
    
    if (origin && allowedOrigins.includes(origin)) {
      return {
        ...defaultHeaders,
        "Access-Control-Allow-Origin": origin,
      };
    }
    
    return defaultHeaders;
  },
});

Environment-Based Configuration

const config = createConfig({
  cors: ({ defaultHeaders }) => {
    if (process.env.NODE_ENV === "production") {
      return {
        ...defaultHeaders,
        "Access-Control-Allow-Origin": "https://myapp.com",
      };
    }
    
    // Allow all origins in development
    return defaultHeaders;
  },
});

Allow Credentials

For requests that include cookies or authentication:
const config = createConfig({
  cors: ({ defaultHeaders }) => ({
    ...defaultHeaders,
    "Access-Control-Allow-Origin": "https://myapp.com",
    "Access-Control-Allow-Credentials": "true",
  }),
});
When using Access-Control-Allow-Credentials: true, you cannot use Access-Control-Allow-Origin: *. You must specify an exact origin.

Custom Allowed Headers

const config = createConfig({
  cors: ({ defaultHeaders }) => ({
    ...defaultHeaders,
    "Access-Control-Allow-Headers": "Content-Type, Authorization, X-Api-Key",
  }),
});

Expose Response Headers

To make custom response headers available to the client:
const config = createConfig({
  cors: ({ defaultHeaders }) => ({
    ...defaultHeaders,
    "Access-Control-Expose-Headers": "X-Total-Count, X-Page-Number",
  }),
});

Async CORS Configuration

Your CORS function can be asynchronous for database lookups or external validation:
const config = createConfig({
  cors: async ({ defaultHeaders, request, logger }) => {
    const origin = request.headers.origin;
    
    // Check if origin is allowed
    const allowed = await db.allowedOrigins.exists({ origin });
    
    if (allowed) {
      logger.info(`CORS: Allowed origin ${origin}`);
      return {
        ...defaultHeaders,
        "Access-Control-Allow-Origin": origin,
      };
    }
    
    logger.warn(`CORS: Rejected origin ${origin}`);
    return defaultHeaders;
  },
});

Endpoint-Specific CORS

If you need different CORS rules for specific endpoints, use middleware or response customization:
import { Middleware } from "express-zod-api";

const publicMiddleware = new Middleware({
  handler: async ({ request, response }) => {
    // Set CORS headers for public endpoints
    response.setHeader("Access-Control-Allow-Origin", "*");
    return {};
  },
});

const publicEndpointsFactory = defaultEndpointsFactory
  .addMiddleware(publicMiddleware);
The global cors configuration applies to all endpoints. For endpoint-specific headers, consider using middleware or response customization.

Preflight Requests

Express Zod API automatically handles OPTIONS preflight requests when CORS is enabled. You don’t need to configure anything special.

Testing CORS

Test CORS with curl:
# Preflight request
curl -X OPTIONS http://localhost:8090/api/users \
  -H "Origin: https://example.com" \
  -H "Access-Control-Request-Method: POST" \
  -v

# Actual request
curl -X POST http://localhost:8090/api/users \
  -H "Origin: https://example.com" \
  -H "Content-Type: application/json" \
  -d '{"name":"John"}' \
  -v
Look for Access-Control-* headers in the response.

Common CORS Headers

HeaderDescription
Access-Control-Allow-OriginWhich origins can access the resource
Access-Control-Allow-MethodsWhich HTTP methods are allowed
Access-Control-Allow-HeadersWhich request headers are allowed
Access-Control-Allow-CredentialsWhether credentials can be sent
Access-Control-Max-AgeHow long preflight results can be cached
Access-Control-Expose-HeadersWhich response headers can be read by client

Security Considerations

Don't Use * in Production

Avoid Access-Control-Allow-Origin: * in production. Specify exact origins.

Validate Origins

When allowing multiple origins, validate them against a whitelist.

Be Careful with Credentials

Only enable credentials for trusted origins. Never combine with *.

Limit Exposed Headers

Only expose headers that clients need. Don’t expose sensitive information.

Complete Example

Here’s a production-ready CORS configuration:
import { createConfig } from "express-zod-api";

const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(",") || [];

const config = createConfig({
  http: { listen: 8090 },
  cors: ({ defaultHeaders, request, logger }) => {
    const origin = request.headers.origin;
    
    // Development: allow all
    if (process.env.NODE_ENV === "development") {
      return {
        ...defaultHeaders,
        "Access-Control-Allow-Credentials": "true",
      };
    }
    
    // Production: whitelist only
    if (origin && allowedOrigins.includes(origin)) {
      logger.debug(`CORS: Allowed ${origin}`);
      return {
        ...defaultHeaders,
        "Access-Control-Allow-Origin": origin,
        "Access-Control-Allow-Credentials": "true",
        "Access-Control-Max-Age": "3600",
      };
    }
    
    logger.warn(`CORS: Rejected ${origin}`);
    return {}; // No CORS headers = request blocked
  },
});

Troubleshooting

CORS Error in Browser Console

If you see:
Access to fetch at 'http://api.example.com' from origin 'http://app.example.com' 
has been blocked by CORS policy
Solutions:
  • Ensure cors: true is set in config
  • Check that the origin is in your allowlist
  • Verify headers are being sent (use browser DevTools)

Credentials Not Working

Requirements for credentials:
  1. Access-Control-Allow-Credentials: true
  2. Access-Control-Allow-Origin must be a specific origin (not *)
  3. Client must send credentials: 'include' in fetch

Next Steps