Skip to content

Tutorial: Set Up CORS for an API

In this tutorial you will add CORS to a Remix API server, configure it for specific origins, handle credentials (cookies), and set up dynamic origin lookup for multi-tenant applications.

Prerequisites

  • A Remix project with a working server (see the Getting Started guide).
  • Basic understanding of HTTP headers.

Step 1: Allow All Origins (Public API)

If you are building a public API that anyone can consume, start with the simplest configuration:

ts
import { createRouter } from 'remix/fetch-router'
import { cors } from 'remix/cors-middleware'
import { route } from 'remix/fetch-router/routes'

let routes = route({
  posts: '/api/posts',
})

let router = createRouter({
  middleware: [
    cors({ origin: '*' }),
  ],
})

router.get(routes.posts, () => {
  return Response.json([
    { id: 1, title: 'First Post' },
    { id: 2, title: 'Second Post' },
  ])
})

With origin: '*', any website can fetch from your API. The middleware adds Access-Control-Allow-Origin: * to every response.

Step 2: Restrict to Specific Origins

For most applications, you want to allow only your own frontend:

ts
cors({
  origin: 'https://myapp.com',
})

Or multiple frontends:

ts
cors({
  origin: [
    'https://myapp.com',
    'https://admin.myapp.com',
    'http://localhost:5173', // Development
  ],
})

When the request's Origin header matches one of these values, the middleware reflects it back. Otherwise, no CORS headers are set and the browser blocks the request.

Step 3: Handle Credentials (Cookies)

If your frontend sends cookies (for authentication), you must enable credentials and use specific origins:

ts
cors({
  origin: 'https://myapp.com',
  credentials: true,
})

This adds Access-Control-Allow-Credentials: true to responses. The browser will include cookies in cross-origin requests, and your session middleware can read them.

Do not use origin: '*' with credentials

Browsers reject responses that combine Access-Control-Allow-Origin: * with Access-Control-Allow-Credentials: true. Always specify exact origins when using credentials.

Step 4: Configure Allowed Methods and Headers

By default, the middleware allows all standard HTTP methods. You can restrict them:

ts
cors({
  origin: 'https://myapp.com',
  methods: ['GET', 'POST'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  exposedHeaders: ['X-Total-Count'],
})
  • methods: Which HTTP methods the client can use.
  • allowedHeaders: Which headers the client can send. Browsers ask about these in preflight requests.
  • exposedHeaders: Which response headers the client-side JavaScript can read. By default, browsers only expose a few safe headers.

Step 5: Cache Preflight Responses

Before making certain cross-origin requests, the browser sends a preflight OPTIONS request to check permissions. You can tell the browser to cache the preflight response so it does not repeat it on every request:

ts
cors({
  origin: 'https://myapp.com',
  credentials: true,
  maxAge: 86400, // Cache preflight for 24 hours
})

The maxAge value is in seconds. The middleware responds to preflight requests with a 204 No Content and the appropriate headers. The browser caches this and skips the preflight for subsequent requests within the cache window.

Step 6: Dynamic Origins for Multi-Tenant Apps

For applications with many tenants or subdomains, use a function to determine allowed origins at runtime:

ts
cors({
  origin(requestOrigin) {
    // Allow any subdomain of myapp.com
    if (requestOrigin?.endsWith('.myapp.com')) {
      return requestOrigin
    }

    // Allow known partner domains from config
    let partners = getPartnerOrigins() // Your lookup logic
    if (requestOrigin && partners.includes(requestOrigin)) {
      return requestOrigin
    }

    return false // Deny all other origins
  },
  credentials: true,
})

The function receives the Origin header value and returns either:

  • The allowed origin string (which is set in the response header).
  • false to deny the request.

Step 7: Verify CORS Headers

Test your configuration with curl:

sh
# Simulate a cross-origin request
curl -I -H "Origin: https://myapp.com" http://localhost:3000/api/posts

Look for these headers in the response:

Access-Control-Allow-Origin: https://myapp.com
Access-Control-Allow-Credentials: true
Vary: Origin

Test a preflight request:

sh
curl -X OPTIONS \
  -H "Origin: https://myapp.com" \
  -H "Access-Control-Request-Method: POST" \
  -H "Access-Control-Request-Headers: Content-Type" \
  http://localhost:3000/api/posts

You should see a 204 response with Access-Control-Allow-Methods and Access-Control-Allow-Headers.

What You Learned

  • How to allow all origins or restrict to specific ones.
  • How to enable credentials (cookies) for cross-origin requests.
  • How to configure allowed methods, headers, and preflight caching.
  • How to use a function for dynamic origin lookup.
  • How to verify CORS configuration with curl.

Next Steps

Released under the MIT License.