Simple vs Preflighted Requests
Learn when browsers send OPTIONS preflight and how to configure server CORS headers correctly
Introduction: Understanding Request Types in CORS
Have you ever wondered why some of your API calls work instantly while others trigger mysterious OPTIONS requests that seem to come out of nowhere? You're implementing what appears to be identical JavaScript fetch calls, yet your browser's network tab shows completely different behavior. One request fires off immediately, while another performs an elaborate "handshake" before your actual request even leaves the browser. This isn't a bugβit's CORS (Cross-Origin Resource Sharing) doing its job, and understanding this behavior is crucial for modern web development. In this lesson, you'll master the distinction between simple requests and preflighted requests, complete with free flashcards to reinforce these critical concepts.
The reason browsers treat cross-origin requests differently isn't arbitraryβit's rooted in a fundamental security challenge. When browsers first introduced the ability for JavaScript to make HTTP requests to other domains, they faced a dilemma: how could they enable powerful new web applications without breaking the security assumptions that countless existing servers relied upon? The answer was to create a two-tier system that distinguishes between requests that were already possible through traditional HTML forms and requests that represent genuinely new capabilities.
π― Key Principle: The distinction between simple and preflighted requests exists to protect servers that were built before CORS existed from receiving requests they weren't designed to handle.
The Historical Context: Legacy Forms and Security Boundaries
Before JavaScript APIs like XMLHttpRequest and fetch() existed, web pages could already send cross-origin requests through HTML forms. You could create a form on attacker.com that submits data to bank.com, and the browser would dutifully send that request. This meant that servers built in the 1990s and early 2000s already had to defend against cross-origin POST requests with form data. They implemented CSRF tokens, checked Referer headers, and used other mechanisms to protect themselves.
But these legacy servers made certain assumptions about what kinds of requests they might receive:
π They expected only GET and POST methods (what forms could do)
π They expected content types like application/x-www-form-urlencoded or multipart/form-data
π They expected only standard headers that browsers automatically send
π They never anticipated receiving PUT, DELETE, or custom headers like X-API-Key
If browsers suddenly allowed JavaScript to send any HTTP method with any headers to any domain, these legacy servers would be exposed to attacks they had no defenses against. A malicious site could send a DELETE request with custom authentication headers before the server even knew what hit it.
π‘ Mental Model: Think of simple requests as "nothing new under the sun"βthey're limited to what attackers could already do with HTML forms in 1999. Preflighted requests represent genuinely new capabilities that require explicit server permission.
The Two-Tier Security System
CORS solves this by categorizing requests into two types:
Simple requests are those that look like something an HTML form could have generated. Since servers were already built to handle these, browsers allow them to proceed immediately. The browser still enforces CORS (checking response headers before letting JavaScript see the response), but it doesn't need to ask permission first.
Preflighted requests are everything elseβrequests using methods beyond GET/POST, custom headers, or unusual content types. For these, the browser sends a preflight OPTIONS request first, asking the server, "Are you prepared to handle this kind of cross-origin request?" Only if the server responds affirmatively does the browser send the actual request.
Simple Request Flow:
-----------------
JavaScript β Browser β Server β Response β JavaScript
(check CORS headers on response)
Preflighted Request Flow:
------------------------
JavaScript β Browser β OPTIONS (preflight) β Server
β Preflight Response β
(check if approved)
β Actual Request β Server
β Response β
(check CORS headers)
β JavaScript
π€ Did you know? The preflight check happens entirely within the browserβyour JavaScript code never sees the OPTIONS request or its response. This is the browser acting as a security intermediary.
Real-World Scenarios
Let's ground this in concrete examples you'll encounter:
Simple Request Scenario: You're building a blog that loads article content from api.blog.com while your frontend runs on www.blog.com. Your JavaScript makes a GET request with only standard headers. Since this is exactly what an <a> tag or form could do, it's a simple requestβit goes through immediately.
π‘ Real-World Example: Google Analytics uses simple requests to send tracking data. The tracking pixel can be loaded from any domain without preflight checks, which is why it's so fast and works even on legacy systems.
Preflighted Request Scenario: You're building a REST API client that needs to send PUT requests to update resources, or you're adding an Authorization: Bearer <token> header to your requests. Neither of these were possible with HTML forms, so the browser sends a preflight OPTIONS request first. You'll see this extra round trip in your network tab, potentially adding 50-200ms to each request.
π‘ Real-World Example: Most modern SPAs (Single Page Applications) that use JWT authentication send custom Authorization headers with every API call, triggering preflight requests. This is why API response time optimization often includes preflight caching strategies.
Impact on Web Application Design
This two-tier system has profound implications for how you design web applications:
π― Performance considerations: Preflight requests add network latency. For high-frequency API calls, you might structure your API to use simple requests where possible, or configure preflight caching.
π― Server configuration complexity: Your server needs different CORS handling for OPTIONS requests versus actual requests. Missing preflight configuration is the #1 cause of CORS errors.
π― Security architecture: Understanding which requests trigger preflights helps you reason about attack surfaces. Simple requests are already part of your threat model whether CORS exists or not.
β οΈ Common Mistake: Developers often think CORS prevents requests from being sent. Actually, simple requests always reach the serverβCORS just prevents JavaScript from reading the response. Only preflight checks can stop a request before it reaches your server. β οΈ
As we move through this lesson, you'll learn the exact criteria that determine whether a request is simple or preflighted, how to debug CORS issues when they arise, and how to design your APIs to work optimally within this security model. The distinction between these request types isn't just academicβit affects every cross-origin HTTP request your web application makes.
Simple Requests: Criteria and Mechanics
When you make a cross-origin HTTP request from a web page, the browser must decide: should I send this request immediately, or should I ask the server for permission first? This decision hinges on whether your request qualifies as a simple request. Understanding the precise criteria is essential because simple requests bypass the preflight checkβthey're sent directly to the server and validated after the fact.
π― Key Principle: Simple requests represent the same kinds of requests that browsers could always make through HTML formsβeven before CORS existed. This backward compatibility is intentional.
The Three Strict Criteria
For a request to qualify as simple, it must satisfy all three of the following conditions simultaneously:
1. HTTP Method Restriction
Only three methods are permitted:
- π§ GET - Retrieving data
- π§ HEAD - Retrieving headers only
- π§ POST - Submitting data
Notably absent are PUT, DELETE, PATCH, and other modern REST methods. These trigger preflight checks.
2. Safe Headers Only
You can only set headers that browsers consider CORS-safe. This includes:
AcceptAccept-LanguageContent-LanguageContent-Type(with restrictionsβsee below)Range(with limitations)
Any custom header like Authorization, X-API-Key, or even Content-Type: application/json immediately disqualifies the request from being simple.
3. Content-Type Restrictions
If you include a Content-Type header, it must be one of these three values:
- π
application/x-www-form-urlencoded - π
multipart/form-data - π
text/plain
Why These Specific Content-Types?
The answer lies in what HTML forms could do before JavaScript's fetch() and XMLHttpRequest existed. Traditional <form> elements could only submit data using these three encodings. By restricting simple requests to these same content types, browsers ensure that CORS doesn't allow any cross-origin request that couldn't already happen through a form submission.
π‘ Mental Model: Think of simple requests as "what a 1990s HTML form could do." If your request goes beyond that capability, it needs explicit server permission via preflight.
β οΈ Common Mistake: Sending Content-Type: application/json and wondering why you see an OPTIONS request. JSON wasn't something HTML forms could send, so it requires preflight! β οΈ
How Browsers Handle Simple Requests
When your JavaScript code makes a simple request, the browser follows this flow:
[Your JS Code] [Target Server]
| |
| 1. fetch('http://api.example') |
|--------------------------------->|
| GET /data HTTP/1.1 |
| Origin: https://myapp.com |
| |
| 2. Process request|
| 3. Check Origin |
| |
|<---------------------------------|
| HTTP/1.1 200 OK |
| Access-Control-Allow-Origin: |
| https://myapp.com |
| |
| 4. Browser validates CORS |
| 5. Response given to JS |
Notice that the request is sent immediatelyβthere's no preliminary OPTIONS request. The browser automatically adds an Origin header indicating where the request originated from. The server receives the actual request, processes it, and must include appropriate CORS headers in its response.
The Role of CORS Response Headers
After the server responds, the browser examines the Access-Control-Allow-Origin header. This header tells the browser which origins are permitted to read the response:
Access-Control-Allow-Origin: https://myapp.com- Only this specific originAccess-Control-Allow-Origin: *- Any origin (only for public APIs without credentials)
If the response lacks this header, or if the value doesn't match your origin, the browser blocks your JavaScript from accessing the response. Critically, the server already processed the requestβyour code just can't see the result.
β οΈ Common Mistake: Thinking simple requests are "safer" because they skip preflight. The server still executes the request! If it's a POST that modifies data, that modification happens even if CORS validation fails. β οΈ
π‘ Real-World Example: Imagine submitting a contact form via POST with application/x-www-form-urlencoded. This is a simple request. The server receives and processes the form submission immediately. If the server forgets to set CORS headers, your JavaScript won't see the success response, but the contact form was still submitted!
Limitations of Simple Requests
While simple requests offer lower latency (no preflight round-trip), they fall short for modern API patterns:
β Cannot send JSON data - Modern REST APIs expect Content-Type: application/json
β Cannot include authentication tokens - Headers like Authorization aren't allowed
β Cannot use modern HTTP verbs - PUT, DELETE, and PATCH require preflight
β Cannot include custom headers - API versioning headers like X-API-Version trigger preflight
β Good for: Read-only GET requests to public APIs, traditional form submissions
β Not suitable for: Authenticated APIs, RESTful CRUD operations, APIs requiring custom headers
π Quick Reference Card: Simple Request Checklist
| Criterion | β Allowed | β Triggers Preflight |
|---|---|---|
| π§ Method | GET, HEAD, POST | PUT, DELETE, PATCH |
| π Content-Type | application/x-www-form-urlencoded, multipart/form-data, text/plain | application/json, application/xml |
| π·οΈ Headers | Accept, Accept-Language, Content-Language | Authorization, X-Custom-Header |
π§ Mnemonic: "GHP" for simple methods - GET, HEAD, POST. Anything fancier needs permission.
Validation Timing: The Critical Difference
The defining characteristic of simple requests is post-flight validation. The request flies to the server first, executes, and only then does the browser check if your JavaScript should be allowed to see the response. This is fundamentally different from preflighted requests, which ask permission before sending the actual request.
This distinction has important security implications: simple requests can trigger server-side effects even if CORS ultimately denies access to the response. For truly sensitive operations, servers must implement additional protection mechanisms beyond CORS alone.
π‘ Pro Tip: If you control both client and server, design your simple requests to be idempotent (safe to repeat) since the browser doesn't check permission first. Save state-changing operations for preflighted requests where you have that extra layer of control.
Preflighted Requests: The OPTIONS Handshake
While simple requests proceed directly to the server, many modern web applications need capabilities that fall outside those narrow constraints. When you send a preflighted requestβone that uses custom headers, methods beyond GET/POST/HEAD, or Content-Type values like application/jsonβthe browser implements a protective two-step process to ensure the target server is prepared for and explicitly permits the incoming request.
π― Key Principle: The preflight mechanism exists to protect servers that were built before CORS existed. Without preflight checks, malicious sites could send DELETE requests or custom authenticated requests to APIs that never anticipated cross-origin traffic.
When Preflight Triggers
The browser automatically initiates a preflight request when any of these conditions apply:
π Non-simple HTTP methods: PUT, DELETE, PATCH, CONNECT, OPTIONS, TRACE
π Custom headers: Any header beyond the simple set (Accept, Accept-Language, Content-Language, Content-Type with restrictions)
π Advanced Content-Type values: application/json, application/xml, text/xml, or anything other than application/x-www-form-urlencoded, multipart/form-data, or text/plain
π‘ Real-World Example: Consider a modern React application calling a REST API:
fetch('https://api.example.com/users/123', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer token123'
},
body: JSON.stringify({ name: 'Alice' })
})
This triggers a preflight because it uses PUT (non-simple method), includes Authorization (custom header), and sends application/json (non-simple content type).
The OPTIONS Request Structure
Before sending your actual request, the browser automatically constructs and sends an OPTIONS request to the same endpoint. This preflight request looks like:
OPTIONS /users/123 HTTP/1.1
Host: api.example.com
Origin: https://myapp.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: authorization, content-type
Notice the browser includes:
- Access-Control-Request-Method: Tells the server which HTTP method the actual request will use
- Access-Control-Request-Headers: Lists all non-simple headers the actual request will include (lowercased, comma-separated)
The browser is essentially asking: "Is it okay if I send a PUT request from myapp.com with these specific headers?"
Server Response Requirements
For the preflight to succeed, the server must respond to the OPTIONS request with specific CORS headers:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://myapp.com
Access-Control-Allow-Methods: GET, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: authorization, content-type
Access-Control-Max-Age: 86400
Access-Control-Allow-Methods explicitly lists which HTTP methods are permitted for cross-origin requests. The actual request method (PUT in our example) must appear in this list.
Access-Control-Allow-Headers lists which custom headers the server accepts. Every header specified in the preflight's Access-Control-Request-Headers must be included here (case-insensitive matching).
Access-Control-Max-Age tells the browser how long (in seconds) to cache this preflight response. During this window, identical requests can skip the preflight check entirely.
β οΈ Common Mistake: Developers often forget that the preflight response needs separate CORS header handling. If your server only adds CORS headers to GET/POST responses, preflights will fail. β οΈ
The Complete Two-Step Sequence
Here's the full flow visualized:
Browser Server
| |
| OPTIONS /api/data |
| (preflight check) |
|----------------------------->|
| |
| 204 No Content |
| Allow-Methods: PUT |
| Allow-Headers: auth |
|<-----------------------------|
| |
| [Preflight approved] |
| |
| PUT /api/data |
| (actual request) |
|----------------------------->|
| |
| 200 OK |
| {"success": true} |
|<-----------------------------|
| |
π‘ Mental Model: Think of preflight as a security guard checking credentials before allowing entry. The OPTIONS request is like showing your ID at the door; only after approval does the actual business transaction (PUT/DELETE/etc.) proceed.
Performance Implications and Optimization
Preflight requests create latencyβevery preflighted operation requires two round trips instead of one. For applications making frequent API calls, this overhead compounds quickly.
π§ Optimization Strategy 1: Maximize Access-Control-Max-Age
Set Access-Control-Max-Age to a high value (86400 seconds = 24 hours is common). Browsers cache preflight results per-URL, so subsequent requests to the same endpoint skip the OPTIONS call entirely during the cache period.
π§ Optimization Strategy 2: Use Simple Requests When Possible
If you can structure your API to accept Content-Type: text/plain and encode JSON as a string, or use POST with form-encoded data, you might avoid preflight altogether. However, this often sacrifices API design quality.
π§ Optimization Strategy 3: Batch Operations
Instead of making multiple preflighted requests, design endpoints that accept batch operations in a single request.
π€ Did you know? Some browsers ignore Access-Control-Max-Age values over 600 seconds (10 minutes) for security reasons, though modern browsers generally respect values up to 24 hours or more.
π Quick Reference Card: Preflight Triggering Conditions
| Condition | Example | Triggers Preflight? |
|---|---|---|
| π§ Method | DELETE | β Yes |
| π§ Method | POST | β No (if simple) |
| π Header | Authorization | β Yes |
| π Header | Accept-Language | β No |
| π Content-Type | application/json | β Yes |
| π Content-Type | text/plain | β No |
Understanding the preflight handshake is essential for debugging CORS issues. When you see OPTIONS requests failing in your browser's network tab, you know the server hasn't properly configured its preflight response headersβthe actual PUT or DELETE never even gets sent.
Common Pitfalls and Debugging Strategies
Even experienced developers stumble over CORS implementation, often because the distinction between simple and preflighted requests is subtle yet consequential. The difference between adding a single header or changing a Content-Type value can completely transform how your browser communicates with the server. Let's explore the most common mistakes and equip you with practical debugging techniques to identify and resolve these issues quickly.
Mistake 1: Accidentally Triggering Preflights β οΈ
The most frequent pitfall occurs when developers unknowingly convert what could be a simple request into a preflighted one. This typically happens in two scenarios:
The Authorization Header Trap: You've built an API that works perfectly in testing, then you add authentication. Suddenly, every request generates two network calls instead of one. Why? The moment you include an Authorization header (even with a simple Bearer token), your request no longer qualifies as "simple" and triggers a preflight OPTIONS request.
β Wrong thinking: "I'm just adding authentication, the request type stays the same."
β
Correct thinking: "Any custom header like Authorization converts my simple request
into a preflighted request, requiring proper OPTIONS handling."
The Content-Type JSON Confusion: Developers often assume that Content-Type: application/json is standard and harmless. However, only three Content-Type values keep a request "simple": application/x-www-form-urlencoded, multipart/form-data, and text/plain. The commonly-used application/json immediately triggers a preflight.
π‘ Pro Tip: If you control both client and server, consider whether you can restructure your API to use simple requests for high-frequency endpoints. For example, encoding JSON data in a form field with Content-Type application/x-www-form-urlencoded avoids the preflight overhead.
Mistake 2: Misunderstanding Simple Request Security β οΈ
A dangerous misconception is that simple requests bypass CORS enforcement entirely. In reality, simple requests still enforce CORSβthey just skip the preflight check. The browser still sends the request with an Origin header, and still validates the server's Access-Control-Allow-Origin response header.
Simple Request Flow:
Client Server
| |
|--GET with Origin: foo.com---->|
| |
|<--Response with CORS headers--| β Browser checks these!
| |
β or β (blocked if headers don't match)
The difference is timing: with simple requests, the browser sends the actual request immediately and validates afterward, potentially allowing side effects on the server even if CORS validation ultimately fails. With preflighted requests, the browser validates first with OPTIONS, preventing the actual request if validation fails.
π― Key Principle: Simple requests protect the browser (client), not the server. The server may execute the request logic before the browser blocks the response from reaching your JavaScript.
Mistake 3: Server Misconfiguration for Preflights β οΈ
When implementing preflight handling, servers must respond to OPTIONS requests with specific headers. A common error is returning CORS headers only for the actual request (GET, POST, etc.) but forgetting to include them in the OPTIONS response:
Required headers in OPTIONS response:
Access-Control-Allow-Origin: Must match the requesting originAccess-Control-Allow-Methods: Must include the actual request methodAccess-Control-Allow-Headers: Must include any custom headers from the request
β οΈ Common Mistake: Returning a 404 or 405 status code for OPTIONS requests because your server framework doesn't handle them by default. The preflight must return 2xx (typically 200 or 204).
Mistake 4: Preflight Cache Confusion β οΈ
Browsers cache preflight responses according to the Access-Control-Max-Age header, which specifies cache duration in seconds. During development, this creates frustrating situations where you fix your server configuration, but the browser continues using the old cached preflight result.
π‘ Pro Tip: While debugging, set Access-Control-Max-Age: 0 or omit it entirely to prevent caching. In production, use values like 86400 (24 hours) to reduce preflight overhead for returning users.
To clear cached preflights during development:
- Chrome/Edge: Open DevTools β Network tab β Right-click β "Clear browser cache"
- Firefox: DevTools β Network β Disable cache checkbox
- Hard refresh (Ctrl+Shift+R or Cmd+Shift+R) may not clear preflight cache
Debugging with Browser DevTools
The Network tab in browser DevTools is your primary weapon for CORS debugging. Here's what to look for:
π§ Identifying Request Type:
- Open Network tab and filter by XHR/Fetch
- Trigger your cross-origin request
- Look for two entries with the same URL if preflighted: one with method "OPTIONS", one with your actual method
- If you see only one entry, it's a simple request
π§ Inspecting CORS Headers: Click on the request β Headers tab:
- Request Headers section: Look for
Origin(browser-added),Access-Control-Request-Method,Access-Control-Request-Headers(in OPTIONS) - Response Headers section: Look for all
Access-Control-*headers - Red text or console errors: Indicate CORS validation failure
π Quick Debugging Checklist:
β Is preflight appearing unexpectedly?
β Check for Authorization header or custom headers
β Verify Content-Type is one of the three simple types
β Is CORS failing on simple request?
β Verify Access-Control-Allow-Origin in response
β Check for credentials mode requiring exact origin match
β Is OPTIONS request failing?
β Ensure server handles OPTIONS method
β Verify all three Allow-* headers present
β Check status code is 2xx
β Fixed server but still broken?
β Clear preflight cache
β Check Access-Control-Max-Age value
π‘ Real-World Example: A developer reported that authentication worked in Postman but failed in the browser. Investigation revealed they were sending Authorization header with Content-Type: application/json, triggering a preflight. Their server handled POST requests correctly but returned 405 for OPTIONS. The fix: add OPTIONS route handler returning proper CORS headers. Postman worked because it doesn't enforce CORSβit's not a browser.
π€ Did you know? The browser console often provides helpful CORS error messages, but they can be misleading. An error saying "No 'Access-Control-Allow-Origin' header present" might actually mean the preflight failed, preventing the actual request from even being attempted.
Summary: Choosing the Right Request Pattern
You've now explored the fundamental mechanics of CORS request types and understand how browsers use the distinction between simple and preflighted requests to balance web functionality with security. This knowledge transforms what might have seemed like arbitrary browser behavior into a coherent security model. Let's consolidate your understanding with practical decision-making guidance.
The Request Type Decision Tree
When designing or debugging cross-origin requests, you can quickly determine whether a preflight will occur by following this logical flow:
Is the request cross-origin?
|
ββ NO β No CORS checks at all
|
ββ YES β Continue...
|
Is the method GET, HEAD, or POST?
|
ββ NO β PREFLIGHTED (PUT, DELETE, PATCH, etc.)
|
ββ YES β Continue...
|
Does it use custom headers beyond:
Accept, Accept-Language, Content-Language,
Content-Type, Range?
|
ββ YES β PREFLIGHTED
|
ββ NO β Continue...
|
Is Content-Type one of:
β’ application/x-www-form-urlencoded
β’ multipart/form-data
β’ text/plain
|
ββ NO β PREFLIGHTED
|
ββ YES β SIMPLE REQUEST
π― Key Principle: A request becomes preflighted the moment it steps outside the narrow boundaries that browsers consider "historically safe" - those that could already be triggered by simple HTML forms.
Security Implications: The Defense Mechanism
Understanding why preflights exist is crucial for secure API design. The preflight OPTIONS request acts as a permission checkpoint that prevents unauthorized cross-origin writes from reaching your server logic.
The threat model preflights address:
Before CORS, browsers enforced the Same-Origin Policy strictly - scripts could only make requests to their own origin. When CORS was introduced, browsers needed to ensure backward compatibility with servers that predated CORS and had no knowledge of cross-origin security. These legacy servers might:
π Accept DELETE requests without authentication headers
π Process JSON payloads that trigger destructive operations
π Trust custom headers like X-Admin: true without validation
Without preflights, a malicious site could send these dangerous requests with your cookies automatically attached. The preflight ensures the server explicitly opts into accepting each type of cross-origin request before any actual data-modifying request reaches it.
π‘ Real-World Example: Imagine a legacy admin API at api.company.com/admin/delete that checks for an X-Admin-Token header. Without preflights, an attacker's site could attempt to send this request with various token values. The preflight stops this attack at the browser level - unless the server explicitly allows the X-Admin-Token header in Access-Control-Allow-Headers, the actual DELETE never gets sent.
β οΈ Critical Point: Simple requests still reach your server even without proper CORS headers - the browser only blocks the response from being read by JavaScript. This is why server-side CSRF protection remains essential even with CORS!
Performance Trade-offs: Optimization Strategies
Every preflighted request incurs two round-trips instead of one: the OPTIONS check, then the actual request. On high-latency connections, this doubles the perceived request time.
When to optimize for simple requests:
π― High-frequency, low-stakes operations - Analytics endpoints, read-only queries, polling endpoints
π― Mobile-first applications - Latency matters significantly more on cellular networks
π― Public APIs with broad access - If you're allowing requests from any origin anyway
Optimization techniques:
- Use
Content-Type: text/plainfor JSON-like data when the structure is simple enough to parse manually - Encode complex data in query parameters for GET requests instead of POST bodies
- Configure
Access-Control-Max-Ageto cache preflight responses (up to 24 hours in most browsers)
When to accept preflight overhead:
π Authentication-required endpoints - You're already using Authorization headers, triggering preflights regardless
π Data-modifying operations - PUT, PATCH, DELETE require preflights automatically
π APIs requiring proper content negotiation - Using Content-Type: application/json signals intent clearly
π‘ Pro Tip: For APIs with mixed use cases, consider offering both patterns: a simple-request endpoint for quick reads (GET with limited query params) and a full REST API accepting JSON for complex operations.
Best Practices for API Design
π Quick Reference Card: API Design Checklist
| Consideration | Simple Request Pattern | Preflighted Pattern |
|---|---|---|
| π― Use Case | Public reads, analytics | Authenticated operations, REST APIs |
| π Security | β οΈ Limited (CSRF tokens required) | β Explicit permission per header/method |
| β‘ Performance | Fast (1 round-trip) | Slower (2 round-trips) |
| π οΈ Developer Experience | β Awkward constraints | β Natural REST patterns |
| π± Mobile-Friendly | β Excellent | β οΈ Acceptable with caching |
| π Caching | Standard HTTP caching | Preflight cache + response cache |
Recommended patterns:
For new APIs: Embrace preflights. Use
application/json, proper HTTP methods, and custom headers. The engineering clarity outweighs the performance cost in most scenarios.For public CDN assets: Design for simple requests. These are often accessed without credentials and benefit from minimizing round-trips.
For hybrid systems: Use simple requests for
GEToperations with standard headers, but don't contort your design to avoid preflights on writes.Always configure
Access-Control-Max-Age: 86400(24 hours) to minimize preflight overhead for returning clients.
π€ Did you know? Some major APIs like Stripe deliberately design their webhook signature verification to work with simple POST requests, allowing webhook consumers to avoid CORS complexity entirely by validating signatures server-side.
What You've Learned
You now understand that:
β
Request type is determined by objective criteria - method, headers, and Content-Type - following a predictable decision tree
β
Preflights exist to protect legacy servers from cross-origin requests they weren't designed to handle
β
Performance differences are measurable but manageable through strategic caching and pattern selection
β
API design should balance security, performance, and developer experience rather than dogmatically avoiding preflights
β οΈ Final Critical Points:
- Simple requests still need CSRF protection - CORS doesn't replace traditional security measures
- Credentials require explicit opt-in -
Access-Control-Allow-Credentials: trueplus specific origins - Preflight caching is per-URL - different endpoints need separate preflight requests initially
Practical Next Steps
Immediate applications:
π§ Audit your existing APIs - Check whether you're accidentally triggering preflights where simple requests would suffice, or vice versa
π§ Implement proper CORS headers - Use the decision tree to predict which headers each endpoint needs
π§ Monitor preflight cache effectiveness - Look at your server logs to see if Access-Control-Max-Age is working as expected
Further exploration:
π Study credential handling - Learn how Access-Control-Allow-Credentials interacts with cookie and authentication flows
π Explore CORS alternatives - Understand when JSONP, server-side proxies, or same-origin architectures might be appropriate
π Master security headers - CORS is one piece of browser security; investigate CSP, COEP, and CORP for a complete picture
You're now equipped to make informed decisions about CORS request patterns, understanding not just the "what" and "how," but the crucial "why" that enables thoughtful API design.