"""
Educational API for Teaching HTTP Methods & Response Formats
============================================================
A simple FastAPI application designed for teaching REST API concepts.
Endpoints cover:
- HTTP Methods: GET, POST, PUT, DELETE, PATCH
- Response Formats: JSON, XML, Plain Text, HTML
- Query Parameters, Path Parameters, Request Bodies
- Headers, Status Codes, and more
"""
from fastapi import FastAPI, Query, Path, Body, Header, Response, HTTPException, Request, File, UploadFile, Form
from fastapi.responses import PlainTextResponse, HTMLResponse, JSONResponse
from pydantic import BaseModel, Field
from typing import Optional, List
from datetime import datetime
import json
import base64
# In-memory storage for uploaded files
uploaded_files_db = {}
app = FastAPI(
title="API Teaching Tool",
description="Educational API for learning HTTP methods, parameters, and response formats",
version="1.0.0",
)
# ============================================================================
# DATA MODELS
# ============================================================================
class Item(BaseModel):
"""A simple item model for POST/PUT examples"""
name: str = Field(..., example="Laptop")
price: float = Field(..., example=999.99)
quantity: int = Field(default=1, example=5)
description: Optional[str] = Field(default=None, example="A powerful laptop")
class User(BaseModel):
"""User model for registration examples"""
username: str = Field(..., example="john_doe")
email: str = Field(..., example="john@example.com")
age: Optional[int] = Field(default=None, example=25)
class Message(BaseModel):
"""Simple message model"""
text: str = Field(..., example="Hello, World!")
# In-memory storage for demo purposes
items_db = {
1: {"id": 1, "name": "Apple", "price": 1.50, "quantity": 100, "description": "Fresh red apple"},
2: {"id": 2, "name": "Banana", "price": 0.75, "quantity": 150, "description": "Yellow banana"},
3: {"id": 3, "name": "Orange", "price": 2.00, "quantity": 80, "description": "Juicy orange"},
}
next_id = 4
# ============================================================================
# ROOT & INFO ENDPOINTS
# ============================================================================
@app.get("/api", tags=["Info"])
def api_info():
"""API overview endpoint"""
return {
"message": "Welcome to the API Teaching Tool!",
"ui": "/",
"documentation": "/docs",
"endpoints": {
"GET examples": ["/hello", "/items", "/items/{id}", "/greet"],
"POST examples": ["/items", "/echo", "/users"],
"PUT examples": ["/items/{id}"],
"DELETE examples": ["/items/{id}"],
"PATCH examples": ["/items/{id}"],
"Form & File Upload": ["/form/login", "/form/contact", "/upload/file", "/upload/files", "/upload/file-with-data"],
"Response formats": ["/format/json", "/format/text", "/format/html", "/format/xml"],
"Educational": ["/echo", "/headers", "/status/{code}"],
}
}
@app.get("/", response_class=HTMLResponse, tags=["Info"])
def ui():
"""Interactive API Explorer UI"""
return """
API Explorer
Copied to clipboard!
Content-Type: application/x-www-form-urlencoded
Content-Type: multipart/form-data
Status-
Time-
Size-
Type-
Send a request to see the response
Send a request to see preview
"""
# ============================================================================
# SIMPLE GET ENDPOINTS
# ============================================================================
@app.get("/hello", tags=["GET Examples"])
def hello():
"""Simplest GET endpoint - returns a greeting"""
return {"message": "Hello, World!"}
@app.get("/time", tags=["GET Examples"])
def get_time():
"""Returns current server time"""
now = datetime.now()
return {
"current_time": now.isoformat(),
"timestamp": now.timestamp(),
"formatted": now.strftime("%B %d, %Y at %I:%M %p")
}
@app.get("/greet", tags=["GET Examples"])
def greet(name: str = Query(default="Guest", description="Your name")):
"""GET with query parameter - /greet?name=YourName"""
return {"message": f"Hello, {name}!", "parameter_received": name}
@app.get("/greet/{name}", tags=["GET Examples"])
def greet_path(name: str = Path(..., description="Your name in the URL path")):
"""GET with path parameter - /greet/YourName"""
return {"message": f"Hello, {name}!", "path_parameter": name}
@app.get("/search", tags=["GET Examples"])
def search(
q: str = Query(..., description="Search query (required)"),
limit: int = Query(default=10, ge=1, le=100, description="Max results"),
offset: int = Query(default=0, ge=0, description="Skip first N results"),
sort: str = Query(default="relevance", description="Sort by: relevance, date, name")
):
"""GET with multiple query parameters - demonstrates pagination"""
return {
"query": q,
"limit": limit,
"offset": offset,
"sort": sort,
"explanation": "This shows how query parameters work for filtering/pagination",
"example_url": f"/search?q={q}&limit={limit}&offset={offset}&sort={sort}"
}
# ============================================================================
# CRUD OPERATIONS ON ITEMS
# ============================================================================
@app.get("/items", tags=["CRUD - Items"])
def list_items():
"""GET all items"""
return {"items": list(items_db.values()), "count": len(items_db)}
@app.get("/items/{item_id}", tags=["CRUD - Items"])
def get_item(item_id: int = Path(..., description="The ID of the item to retrieve")):
"""GET a single item by ID"""
if item_id not in items_db:
raise HTTPException(status_code=404, detail=f"Item with id {item_id} not found")
return items_db[item_id]
@app.post("/items", status_code=201, tags=["CRUD - Items"])
def create_item(item: Item):
"""POST - Create a new item"""
global next_id
new_item = {"id": next_id, **item.model_dump()}
items_db[next_id] = new_item
next_id += 1
return {"message": "Item created successfully", "item": new_item}
@app.put("/items/{item_id}", tags=["CRUD - Items"])
def update_item(item_id: int, item: Item):
"""PUT - Replace an entire item"""
if item_id not in items_db:
raise HTTPException(status_code=404, detail=f"Item with id {item_id} not found")
updated_item = {"id": item_id, **item.model_dump()}
items_db[item_id] = updated_item
return {"message": "Item updated successfully", "item": updated_item}
@app.patch("/items/{item_id}", tags=["CRUD - Items"])
def partial_update_item(
item_id: int,
name: Optional[str] = Body(default=None),
price: Optional[float] = Body(default=None),
quantity: Optional[int] = Body(default=None),
description: Optional[str] = Body(default=None)
):
"""PATCH - Partially update an item (only send fields to change)"""
if item_id not in items_db:
raise HTTPException(status_code=404, detail=f"Item with id {item_id} not found")
current_item = items_db[item_id]
if name is not None:
current_item["name"] = name
if price is not None:
current_item["price"] = price
if quantity is not None:
current_item["quantity"] = quantity
if description is not None:
current_item["description"] = description
return {"message": "Item partially updated", "item": current_item}
@app.delete("/items/{item_id}", tags=["CRUD - Items"])
def delete_item(item_id: int):
"""DELETE - Remove an item"""
if item_id not in items_db:
raise HTTPException(status_code=404, detail=f"Item with id {item_id} not found")
deleted_item = items_db.pop(item_id)
return {"message": "Item deleted successfully", "deleted_item": deleted_item}
# ============================================================================
# POST EXAMPLES
# ============================================================================
@app.post("/users", status_code=201, tags=["POST Examples"])
def create_user(user: User):
"""POST - Create a new user (demonstrates JSON body)"""
return {
"message": "User created successfully",
"user": user.model_dump(),
"note": "In a real app, this would save to a database"
}
@app.post("/echo", tags=["POST Examples"])
def echo_body(data: dict = Body(...)):
"""POST - Echo back whatever JSON you send"""
return {
"received": data,
"type": str(type(data).__name__),
"keys": list(data.keys()) if isinstance(data, dict) else None
}
@app.post("/echo/text", tags=["POST Examples"])
async def echo_text(request: Request):
"""POST - Echo back raw text body"""
body = await request.body()
return {
"received": body.decode("utf-8"),
"length": len(body),
"content_type": request.headers.get("content-type", "not specified")
}
# ============================================================================
# FORM & FILE UPLOAD
# ============================================================================
next_file_id = 1
@app.post("/form/login", tags=["Form & File Upload"])
async def form_login(
username: str = Form(..., description="Username"),
password: str = Form(..., description="Password"),
remember_me: bool = Form(default=False, description="Remember me checkbox")
):
"""POST - Handle form data (like a login form)"""
return {
"message": "Form data received successfully",
"data": {
"username": username,
"password": "***hidden***",
"password_length": len(password),
"remember_me": remember_me
},
"note": "This demonstrates form-urlencoded data (Content-Type: application/x-www-form-urlencoded)"
}
@app.post("/form/contact", tags=["Form & File Upload"])
async def form_contact(
name: str = Form(..., description="Your name"),
email: str = Form(..., description="Your email"),
subject: str = Form(default="General Inquiry", description="Subject"),
message: str = Form(..., description="Your message")
):
"""POST - Contact form submission"""
return {
"message": "Contact form received",
"data": {
"name": name,
"email": email,
"subject": subject,
"message": message,
"message_length": len(message)
},
"timestamp": datetime.now().isoformat()
}
@app.post("/upload/file", tags=["Form & File Upload"])
async def upload_single_file(
file: UploadFile = File(..., description="File to upload")
):
"""POST - Upload a single file"""
global next_file_id
contents = await file.read()
file_size = len(contents)
# Store file metadata and content
file_id = next_file_id
uploaded_files_db[file_id] = {
"id": file_id,
"filename": file.filename,
"content_type": file.content_type,
"size": file_size,
"content": base64.b64encode(contents).decode("utf-8")
}
next_file_id += 1
return {
"message": "File uploaded successfully",
"file": {
"id": file_id,
"filename": file.filename,
"content_type": file.content_type,
"size": file_size,
"size_formatted": f"{file_size / 1024:.2f} KB" if file_size > 1024 else f"{file_size} bytes"
},
"download_url": f"/upload/file/{file_id}"
}
@app.post("/upload/files", tags=["Form & File Upload"])
async def upload_multiple_files(
files: List[UploadFile] = File(..., description="Multiple files to upload")
):
"""POST - Upload multiple files at once"""
global next_file_id
uploaded = []
for file in files:
contents = await file.read()
file_size = len(contents)
file_id = next_file_id
uploaded_files_db[file_id] = {
"id": file_id,
"filename": file.filename,
"content_type": file.content_type,
"size": file_size,
"content": base64.b64encode(contents).decode("utf-8")
}
next_file_id += 1
uploaded.append({
"id": file_id,
"filename": file.filename,
"content_type": file.content_type,
"size": file_size
})
return {
"message": f"Uploaded {len(uploaded)} files successfully",
"files": uploaded,
"total_size": sum(f["size"] for f in uploaded)
}
@app.post("/upload/file-with-data", tags=["Form & File Upload"])
async def upload_file_with_form_data(
file: UploadFile = File(..., description="File to upload"),
title: str = Form(..., description="Title for the file"),
description: str = Form(default="", description="Optional description"),
category: str = Form(default="general", description="Category")
):
"""POST - Upload file with additional form fields (multipart/form-data)"""
global next_file_id
contents = await file.read()
file_size = len(contents)
file_id = next_file_id
uploaded_files_db[file_id] = {
"id": file_id,
"filename": file.filename,
"content_type": file.content_type,
"size": file_size,
"content": base64.b64encode(contents).decode("utf-8"),
"metadata": {
"title": title,
"description": description,
"category": category
}
}
next_file_id += 1
return {
"message": "File uploaded with metadata",
"file": {
"id": file_id,
"filename": file.filename,
"content_type": file.content_type,
"size": file_size
},
"metadata": {
"title": title,
"description": description,
"category": category
},
"download_url": f"/upload/file/{file_id}"
}
@app.get("/upload/files", tags=["Form & File Upload"])
def list_uploaded_files():
"""GET - List all uploaded files"""
files = [
{
"id": f["id"],
"filename": f["filename"],
"content_type": f["content_type"],
"size": f["size"],
"metadata": f.get("metadata")
}
for f in uploaded_files_db.values()
]
return {"files": files, "count": len(files)}
@app.get("/upload/file/{file_id}", tags=["Form & File Upload"])
def download_file(file_id: int = Path(..., description="File ID to download")):
"""GET - Download an uploaded file by ID"""
if file_id not in uploaded_files_db:
raise HTTPException(status_code=404, detail=f"File with id {file_id} not found")
file_data = uploaded_files_db[file_id]
content = base64.b64decode(file_data["content"])
return Response(
content=content,
media_type=file_data["content_type"],
headers={
"Content-Disposition": f'attachment; filename="{file_data["filename"]}"'
}
)
@app.delete("/upload/file/{file_id}", tags=["Form & File Upload"])
def delete_uploaded_file(file_id: int = Path(..., description="File ID to delete")):
"""DELETE - Delete an uploaded file"""
if file_id not in uploaded_files_db:
raise HTTPException(status_code=404, detail=f"File with id {file_id} not found")
deleted = uploaded_files_db.pop(file_id)
return {
"message": "File deleted successfully",
"deleted_file": {
"id": deleted["id"],
"filename": deleted["filename"]
}
}
# ============================================================================
# DIFFERENT RESPONSE FORMATS
# ============================================================================
@app.get("/format/json", tags=["Response Formats"])
def format_json():
"""Returns data as JSON (default)"""
return {
"format": "JSON",
"content_type": "application/json",
"data": {"name": "Alice", "age": 30, "city": "Mumbai"}
}
@app.get("/format/text", response_class=PlainTextResponse, tags=["Response Formats"])
def format_text():
"""Returns data as plain text"""
return """Format: Plain Text
Content-Type: text/plain
Name: Alice
Age: 30
City: Mumbai
This is plain text - no special formatting!"""
@app.get("/format/html", response_class=HTMLResponse, tags=["Response Formats"])
def format_html():
"""Returns data as HTML"""
return """
HTML Response
HTML Response Example
User Info
Name: Alice
Age: 30
City: Mumbai
Content-Type: text/html
"""
@app.get("/format/xml", tags=["Response Formats"])
def format_xml():
"""Returns data as XML"""
xml_content = """
XML
application/xml
Alice
30
Mumbai
"""
return Response(content=xml_content, media_type="application/xml")
@app.get("/format/csv", tags=["Response Formats"])
def format_csv():
"""Returns data as CSV (spreadsheet format)"""
csv_content = """id,name,price,quantity,description
1,Apple,1.50,100,Fresh red apple
2,Banana,0.75,150,Yellow banana
3,Orange,2.00,80,Juicy orange"""
return Response(
content=csv_content,
media_type="text/csv",
headers={"Content-Disposition": "inline; filename=items.csv"}
)
@app.get("/format/markdown", tags=["Response Formats"])
def format_markdown():
"""Returns data as Markdown"""
md_content = """# User Profile
| Field | Value |
|-------|-------|
| Name | Alice |
| Age | 30 |
| City | Mumbai |
## About
This is a **markdown** formatted response.
- Supports *italic* text
- Supports **bold** text
- Supports `code` blocks
```python
print("Hello, World!")
```
"""
return Response(content=md_content, media_type="text/markdown")
@app.get("/format/yaml", tags=["Response Formats"])
def format_yaml():
"""Returns data as YAML"""
yaml_content = """# User Data in YAML format
format: YAML
content_type: application/x-yaml
data:
user:
name: Alice
age: 30
city: Mumbai
hobbies:
- reading
- coding
- hiking
"""
return Response(content=yaml_content, media_type="application/x-yaml")
@app.get("/format/image", tags=["Response Formats"])
def format_image():
"""Returns a tiny PNG image (1x1 red pixel)"""
# Minimal valid PNG: 1x1 red pixel
import base64
png_base64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg=="
png_bytes = base64.b64decode(png_base64)
return Response(content=png_bytes, media_type="image/png")
@app.get("/format/binary", tags=["Response Formats"])
def format_binary():
"""Returns raw binary data (demonstrates non-text response)"""
# Some example bytes
binary_data = bytes([0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x00, 0xFF, 0xFE, 0x00, 0x01])
return Response(
content=binary_data,
media_type="application/octet-stream",
headers={"Content-Disposition": "inline; filename=data.bin"}
)
# ============================================================================
# EDUCATIONAL ENDPOINTS
# ============================================================================
@app.get("/headers", tags=["Educational"])
def show_headers(request: Request):
"""Shows all request headers sent by the client"""
headers_dict = dict(request.headers)
return {
"your_headers": headers_dict,
"common_headers_explained": {
"user-agent": "Identifies your browser/client",
"accept": "What content types you accept",
"host": "The server you're connecting to",
"content-type": "Format of data you're sending (for POST/PUT)"
}
}
@app.get("/headers/custom", tags=["Educational"])
def custom_header_response():
"""Returns a response with custom headers"""
content = {"message": "Check the response headers!"}
headers = {
"X-Custom-Header": "Hello from the server!",
"X-Request-Time": datetime.now().isoformat(),
"X-API-Version": "1.0.0"
}
return JSONResponse(content=content, headers=headers)
@app.get("/status/{code}", tags=["Educational"])
def return_status_code(
code: int = Path(..., description="HTTP status code to return (100-599)")
):
"""Returns the specified HTTP status code - useful for testing error handling"""
status_messages = {
200: "OK - Request succeeded",
201: "Created - Resource created successfully",
204: "No Content - Success but no content to return",
400: "Bad Request - Invalid request syntax",
401: "Unauthorized - Authentication required",
403: "Forbidden - Access denied",
404: "Not Found - Resource doesn't exist",
405: "Method Not Allowed - Wrong HTTP method",
500: "Internal Server Error - Server error",
502: "Bad Gateway - Invalid response from upstream",
503: "Service Unavailable - Server temporarily unavailable",
}
if code < 100 or code > 599:
raise HTTPException(status_code=400, detail="Status code must be between 100 and 599")
message = status_messages.get(code, f"Status code {code}")
if code >= 400:
raise HTTPException(status_code=code, detail=message)
return JSONResponse(
status_code=code,
content={"status_code": code, "message": message}
)
@app.api_route("/method", methods=["GET", "POST", "PUT", "DELETE", "PATCH"], tags=["Educational"])
async def show_method(request: Request):
"""Accepts any HTTP method and shows what was used"""
body = None
if request.method in ["POST", "PUT", "PATCH"]:
try:
body = await request.json()
except:
body = (await request.body()).decode("utf-8") or None
return {
"method_used": request.method,
"method_explanation": {
"GET": "Retrieve data (no body)",
"POST": "Create new resource (has body)",
"PUT": "Replace entire resource (has body)",
"PATCH": "Partial update (has body)",
"DELETE": "Remove resource (usually no body)"
}.get(request.method, "Unknown method"),
"body_received": body,
"url": str(request.url),
"query_params": dict(request.query_params)
}
@app.get("/ip", tags=["Educational"])
def get_client_ip(request: Request):
"""Returns the client's IP address"""
forwarded = request.headers.get("x-forwarded-for")
if forwarded:
ip = forwarded.split(",")[0].strip()
else:
ip = request.client.host if request.client else "unknown"
return {
"ip": ip,
"note": "Behind a proxy, this shows the proxy's IP unless X-Forwarded-For is set"
}
# ============================================================================
# QUERY PARAMETER TYPES
# ============================================================================
@app.get("/params/types", tags=["Parameter Examples"])
def parameter_types(
string_param: str = Query(default="hello", description="A string parameter"),
int_param: int = Query(default=42, description="An integer parameter"),
float_param: float = Query(default=3.14, description="A float parameter"),
bool_param: bool = Query(default=True, description="A boolean parameter"),
list_param: List[str] = Query(default=["a", "b"], description="A list parameter")
):
"""Demonstrates different query parameter types"""
return {
"parameters_received": {
"string_param": {"value": string_param, "type": "str"},
"int_param": {"value": int_param, "type": "int"},
"float_param": {"value": float_param, "type": "float"},
"bool_param": {"value": bool_param, "type": "bool"},
"list_param": {"value": list_param, "type": "list"}
},
"example_url": "/params/types?string_param=test&int_param=100&float_param=2.5&bool_param=false&list_param=x&list_param=y"
}
# ============================================================================
# RUN SERVER (for local development)
# ============================================================================
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=7860)