CORS & REST APIs
When your frontend (running on one domain) needs to call your API (running on another domain), you encounter CORS. Understanding CORS and REST conventions is essential for building web applications that actually work in a browser.
What is CORS?
Cross-Origin Resource Sharing (CORS) is a browser security mechanism. When JavaScript on https://myapp.com tries to fetch data from https://api.myapp.com, the browser blocks the request unless the API explicitly allows it.
The Same-Origin Policy
By default, browsers only allow requests to the same origin (protocol + domain + port). A request from https://myapp.com to https://api.myapp.com is cross-origin because the domains differ.
How CORS Works
- The browser sends a preflight request (OPTIONS) before the actual request
- The server responds with CORS headers indicating what is allowed
- If the headers permit the request, the browser sends the actual request
- If not, the browser blocks it and you see a CORS error in the console
Browser API Server
| |
| OPTIONS /data (preflight) |
| Origin: https://myapp.com |
| Access-Control-Request-Method: POST |
| ------------------------------------>|
| |
| 204 No Content |
| Access-Control-Allow-Origin: |
| https://myapp.com |
| Access-Control-Allow-Methods: |
| GET, POST, PUT, DELETE |
| Access-Control-Allow-Headers: |
| Content-Type, Authorization |
| <------------------------------------|
| |
| POST /data (actual request) |
| ------------------------------------>|
| 200 OK |
| <------------------------------------|
Configuring CORS in FastAPI
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
# Development: allow all origins
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # Allow all domains
allow_credentials=True,
allow_methods=["*"], # Allow all HTTP methods
allow_headers=["*"], # Allow all headers
)
# Production: allow specific origins only
app.add_middleware(
CORSMiddleware,
allow_origins=[
"https://myapp.com",
"https://www.myapp.com",
],
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE"],
allow_headers=["Authorization", "Content-Type"],
)
Wildcard origins with allow_credentials=True is a security risk. Always specify exact origins in production. If you need credentials (cookies, auth headers), the browser requires specific origins — it will not accept *.
REST API Conventions
REST (Representational State Transfer) is a set of conventions for designing APIs. Following REST makes your API intuitive and predictable.
Resource-Based URLs
GET /api/v1/users → List all users
GET /api/v1/users/42 → Get user 42
POST /api/v1/users → Create a new user
PUT /api/v1/users/42 → Replace user 42 entirely
PATCH /api/v1/users/42 → Update user 42 partially
DELETE /api/v1/users/42 → Delete user 42
GET /api/v1/users/42/orders → List orders for user 42
POST /api/v1/users/42/orders → Create order for user 42
HTTP Status Codes
Use the correct status code to communicate the result:
| Code | Meaning | When to Use |
|---|---|---|
| 200 | OK | Successful GET, PUT, PATCH |
| 201 | Created | Successful POST (resource created) |
| 204 | No Content | Successful DELETE |
| 400 | Bad Request | Invalid input, missing fields |
| 401 | Unauthorized | Missing or invalid authentication |
| 403 | Forbidden | Authenticated but not authorized |
| 404 | Not Found | Resource does not exist |
| 409 | Conflict | Duplicate resource |
| 422 | Unprocessable Entity | Validation error |
| 429 | Too Many Requests | Rate limit exceeded |
| 500 | Internal Server Error | Unexpected server failure |
Pagination Pattern
from fastapi import FastAPI, Query
from typing import Optional
app = FastAPI()
@app.get("/api/v1/items")
def list_items(
page: int = Query(1, ge=1),
per_page: int = Query(20, ge=1, le=100),
search: Optional[str] = None,
):
# Calculate offset
offset = (page - 1) * per_page
# Query database
items = get_items(offset=offset, limit=per_page, search=search)
total = count_items(search=search)
return {
"items": items,
"pagination": {
"page": page,
"per_page": per_page,
"total": total,
"total_pages": (total + per_page - 1) // per_page,
}
}
Prefix your API paths with a version number (/api/v1/). When you need to make breaking changes, create /api/v2/ while keeping v1 running. This prevents breaking existing clients.