Access-Control-Allow-Origin to permit or deny cross-origin requests./api/v1/) to ensure backward compatibility./users).An API is a contract: it names the operations a system offers, the shape of the data each one accepts and returns, and the errors it can produce. Implementing one well means more than writing route handlers. It needs authentication and authorisation, input validation, consistent error responses, versioning, pagination for large collections, rate limiting, and documentation that matches the code. The key topics above list these principles; the sections below show how they fit into the request path and how they look in working code.
A request passes through layers that each enforce one concern. TLS terminates at a load balancer or gateway, which may also apply rate limits and CORS rules. Authentication verifies who the caller is, typically by validating a JWT signature or an OAuth 2.0 access token; authorisation then decides whether that caller may act on the requested resource. Validation checks the body and parameters against a schema before business logic runs. Keeping each concern in its own middleware or dependency keeps handlers small and makes the rejection behaviour consistent across endpoints.
| Style | Strengths | Weaknesses |
|---|---|---|
| REST | Simple, cacheable, universal tooling | Over- or under-fetching; many round trips |
| GraphQL | One endpoint, clients choose fields | Caching and rate limiting are harder; query cost control needed |
| gRPC | Compact binary, streaming, generated clients | Not browser-native; harder to inspect by hand |
| SOAP | Formal contracts, WS-Security | Verbose XML, heavy tooling |
# pip install fastapi uvicorn
from fastapi import FastAPI, HTTPException, Depends, Header, Query
from pydantic import BaseModel, Field
app = FastAPI(title="Users API", version="1.0")
DB: dict[int, dict] = {}
class UserIn(BaseModel):
name: str = Field(min_length=1, max_length=80)
email: str
def require_token(authorization: str = Header(...)):
if authorization != "Bearer demo-token": # replace with real JWT validation
raise HTTPException(status_code=401, detail="invalid token")
@app.post("/api/v1/users", status_code=201, dependencies=[Depends(require_token)])
def create_user(user: UserIn):
uid = len(DB) + 1
DB[uid] = {"id": uid, **user.model_dump()}
return DB[uid]
@app.get("/api/v1/users")
def list_users(limit: int = Query(20, le=100), offset: int = 0):
items = list(DB.values())[offset:offset + limit]
return {"items": items, "total": len(DB), "limit": limit, "offset": offset}
@app.get("/api/v1/users/{uid}")
def get_user(uid: int):
if uid not in DB:
raise HTTPException(status_code=404, detail="user not found")
return DB[uid]
uvicorn app:app --reload
curl -X POST localhost:8000/api/v1/users -H 'Authorization: Bearer demo-token' \
-H 'Content-Type: application/json' -d '{"name":"Ada","email":"ada@example.com"}'
curl 'localhost:8000/api/v1/users?limit=10'
# interactive OpenAPI docs: http://localhost:8000/docs
The example applies the principles directly: a versioned path, resource-based URLs, a 201 status on creation, schema validation from the Pydantic model (invalid input returns 422 automatically), a reusable auth dependency, pagination with a capped limit, and generated OpenAPI documentation.
| Code | Meaning | Typical cause |
|---|---|---|
| 200 / 201 / 204 | OK / Created / No Content | Success |
| 400 / 422 | Bad request / validation failed | Malformed JSON, schema violation |
| 401 / 403 | Unauthenticated / forbidden | Missing token / lacks permission |
| 404 / 409 | Not found / conflict | Unknown ID / duplicate or version clash |
| 429 | Too many requests | Rate limit exceeded; include Retry-After |
| 500 / 503 | Server error / unavailable | Bug / overload or maintenance |
Return errors in one consistent JSON shape, for example {"error": {"code": "user_not_found", "message": "..."}}, so clients can branch on a stable code rather than parsing messages.
Access-Control-Allow-Origin: * together with credentials.