api-testing / app.py
Nipun's picture
Add form data and file upload support with UI controls
28a953c
"""
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 """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>API Explorer</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Helvetica Neue', sans-serif;
background: #f5f5f7;
min-height: 100vh;
color: #1d1d1f;
font-size: 14px;
line-height: 1.5;
}
.container { max-width: 1400px; margin: 0 auto; padding: 20px; }
header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 0;
border-bottom: 1px solid #d2d2d7;
margin-bottom: 24px;
}
h1 { font-size: 21px; font-weight: 600; }
.header-links a {
color: #06c;
text-decoration: none;
margin-left: 24px;
font-size: 13px;
}
.header-links a:hover { text-decoration: underline; }
/* Request Bar */
.request-bar {
display: flex;
gap: 8px;
background: #fff;
padding: 8px;
border-radius: 10px;
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
margin-bottom: 20px;
}
select, input, textarea, button {
font-family: inherit;
font-size: 14px;
border: none;
outline: none;
}
.method-select {
padding: 10px 12px;
background: #f5f5f7;
border-radius: 6px;
font-weight: 600;
cursor: pointer;
color: #1d1d1f;
}
.url-input {
flex: 1;
padding: 10px 12px;
background: #f5f5f7;
border-radius: 6px;
font-family: 'SF Mono', Menlo, monospace;
}
.send-btn {
padding: 10px 24px;
background: #007aff;
color: #fff;
font-weight: 500;
border-radius: 6px;
cursor: pointer;
transition: background 0.2s;
}
.send-btn:hover { background: #0056b3; }
.send-btn:disabled { background: #86868b; cursor: not-allowed; }
/* Copy dropdown */
.copy-dropdown {
position: relative;
}
.copy-btn {
padding: 10px 16px;
background: #fff;
border: 1px solid #d2d2d7;
border-radius: 6px;
cursor: pointer;
font-weight: 500;
color: #1d1d1f;
transition: all 0.2s;
}
.copy-btn:hover { background: #f5f5f7; border-color: #007aff; }
.copy-menu {
display: none;
position: absolute;
top: 100%;
right: 0;
margin-top: 4px;
background: #fff;
border: 1px solid #d2d2d7;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
z-index: 100;
min-width: 160px;
overflow: hidden;
}
.copy-menu.show { display: block; }
.copy-option {
display: block;
width: 100%;
padding: 10px 14px;
text-align: left;
background: none;
border: none;
cursor: pointer;
font-size: 13px;
color: #1d1d1f;
transition: background 0.1s;
}
.copy-option:hover { background: #f5f5f7; }
.copy-option-label { font-weight: 500; }
.copy-option-desc { font-size: 11px; color: #86868b; }
/* Toast notification */
.toast {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%) translateY(100px);
background: #1d1d1f;
color: #fff;
padding: 12px 24px;
border-radius: 8px;
font-size: 13px;
opacity: 0;
transition: all 0.3s;
z-index: 1000;
}
.toast.show { transform: translateX(-50%) translateY(0); opacity: 1; }
/* Main Layout */
.main-layout {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
@media (max-width: 1000px) { .main-layout { grid-template-columns: 1fr; } }
.panel {
background: #fff;
border-radius: 10px;
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
overflow: hidden;
}
/* Tabs */
.tabs {
display: flex;
border-bottom: 1px solid #d2d2d7;
background: #fafafa;
}
.tab {
padding: 10px 16px;
font-size: 12px;
font-weight: 500;
color: #86868b;
cursor: pointer;
border-bottom: 2px solid transparent;
margin-bottom: -1px;
background: none;
transition: color 0.2s;
}
.tab:hover { color: #1d1d1f; }
.tab.active { color: #007aff; border-bottom-color: #007aff; }
.tab-content { display: none; }
.tab-content.active { display: block; }
.panel-body { padding: 16px; }
textarea {
width: 100%;
min-height: 180px;
padding: 12px;
background: #f5f5f7;
border-radius: 6px;
resize: vertical;
font-family: 'SF Mono', Menlo, monospace;
font-size: 13px;
line-height: 1.6;
color: #1d1d1f;
}
/* Response Panel */
.status-bar {
display: flex;
gap: 20px;
padding: 12px 16px;
background: #fafafa;
border-bottom: 1px solid #d2d2d7;
font-size: 12px;
}
.status-item { color: #86868b; }
.status-value { font-weight: 600; color: #1d1d1f; margin-left: 6px; }
.status-ok { color: #34c759; }
.status-err { color: #ff3b30; }
.status-warn { color: #ff9500; }
.response-content {
padding: 16px;
font-family: 'SF Mono', Menlo, monospace;
font-size: 12px;
line-height: 1.7;
overflow-x: auto;
white-space: pre-wrap;
word-break: break-word;
max-height: 500px;
overflow-y: auto;
background: #fff;
}
/* Headers display */
.headers-list {
font-family: 'SF Mono', Menlo, monospace;
font-size: 12px;
}
.header-row {
display: flex;
padding: 6px 0;
border-bottom: 1px solid #f0f0f0;
}
.header-row:last-child { border-bottom: none; }
.header-name {
width: 200px;
flex-shrink: 0;
font-weight: 600;
color: #86868b;
}
.header-value { color: #1d1d1f; word-break: break-all; }
/* Examples */
.examples-section { margin-top: 20px; }
.examples-section h3 {
font-size: 13px;
font-weight: 600;
color: #86868b;
margin-bottom: 12px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.examples-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 8px;
}
.example-btn {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
background: #fff;
border: 1px solid #d2d2d7;
border-radius: 8px;
cursor: pointer;
text-align: left;
transition: all 0.15s;
}
.example-btn:hover {
border-color: #007aff;
background: #f5f5f7;
}
.method-badge {
padding: 2px 6px;
border-radius: 4px;
font-size: 10px;
font-weight: 700;
font-family: 'SF Mono', Menlo, monospace;
}
.method-GET { background: #e8f5e9; color: #2e7d32; }
.method-POST { background: #fff3e0; color: #ef6c00; }
.method-PUT { background: #e3f2fd; color: #1565c0; }
.method-DELETE { background: #ffebee; color: #c62828; }
.method-PATCH { background: #f3e5f5; color: #7b1fa2; }
.example-info { flex: 1; min-width: 0; }
.example-title { font-weight: 500; font-size: 13px; }
.example-url {
font-size: 11px;
color: #86868b;
font-family: 'SF Mono', Menlo, monospace;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* JSON highlighting - subtle */
.json-key { color: #007aff; }
.json-string { color: #c41a16; }
.json-number { color: #1c00cf; }
.json-boolean { color: #aa0d91; }
.json-null { color: #86868b; }
/* Form fields */
.form-group { margin-bottom: 12px; }
.form-label {
display: block;
font-size: 12px;
font-weight: 500;
color: #86868b;
margin-bottom: 4px;
}
.form-input {
width: 100%;
padding: 8px 12px;
background: #f5f5f7;
border: 1px solid transparent;
border-radius: 6px;
font-size: 14px;
transition: border-color 0.2s;
}
.form-input:focus { border-color: #007aff; outline: none; }
.form-row { display: flex; gap: 12px; }
.form-row .form-group { flex: 1; }
.checkbox-group {
display: flex;
align-items: center;
gap: 8px;
}
.checkbox-group input { width: 16px; height: 16px; }
/* File upload */
.file-upload-area {
border: 2px dashed #d2d2d7;
border-radius: 8px;
padding: 24px;
text-align: center;
transition: all 0.2s;
cursor: pointer;
background: #fafafa;
}
.file-upload-area:hover { border-color: #007aff; background: #f0f7ff; }
.file-upload-area.dragover { border-color: #007aff; background: #e8f4ff; }
.file-upload-icon { font-size: 32px; margin-bottom: 8px; }
.file-upload-text { font-size: 13px; color: #86868b; }
.file-upload-text strong { color: #007aff; }
.file-input { display: none; }
.file-list { margin-top: 12px; }
.file-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
background: #f5f5f7;
border-radius: 6px;
margin-bottom: 6px;
font-size: 13px;
}
.file-item-name { font-weight: 500; }
.file-item-size { color: #86868b; font-size: 11px; }
.file-item-remove {
background: none;
border: none;
color: #ff3b30;
cursor: pointer;
font-size: 16px;
padding: 0 4px;
}
/* Code snippet panel */
.code-panel {
margin-top: 20px;
background: #fff;
border-radius: 10px;
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
overflow: hidden;
display: none;
}
.code-panel.show { display: block; }
.code-panel-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
background: #fafafa;
border-bottom: 1px solid #d2d2d7;
}
.code-panel-title { font-weight: 600; font-size: 13px; }
.code-panel-close {
background: none;
border: none;
font-size: 18px;
color: #86868b;
cursor: pointer;
padding: 0 4px;
}
.code-panel-close:hover { color: #1d1d1f; }
.code-panel-content {
padding: 16px;
font-family: 'SF Mono', Menlo, monospace;
font-size: 13px;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-all;
background: #1d1d1f;
color: #f5f5f7;
max-height: 300px;
overflow: auto;
}
.code-panel-actions {
padding: 12px 16px;
background: #fafafa;
border-top: 1px solid #d2d2d7;
display: flex;
gap: 8px;
}
.code-copy-btn {
padding: 6px 12px;
background: #007aff;
color: #fff;
border: none;
border-radius: 6px;
font-size: 12px;
cursor: pointer;
}
.code-copy-btn:hover { background: #0056b3; }
/* Preview */
.preview-container {
padding: 16px;
min-height: 200px;
max-height: 500px;
overflow: auto;
}
.preview-placeholder { color: #86868b; }
.preview-iframe {
width: 100%;
min-height: 400px;
border: 1px solid #d2d2d7;
border-radius: 6px;
background: #fff;
}
.preview-image {
max-width: 100%;
border-radius: 6px;
border: 1px solid #d2d2d7;
}
.preview-json {
background: #f5f5f7;
padding: 16px;
border-radius: 8px;
font-family: 'SF Mono', Menlo, monospace;
font-size: 13px;
line-height: 1.6;
}
.preview-json .key { color: #007aff; font-weight: 500; }
.preview-json .string { color: #c41a16; }
.preview-json .number { color: #1c00cf; }
.preview-json .boolean { color: #aa0d91; }
.preview-json .null { color: #86868b; font-style: italic; }
.preview-json .bracket { color: #1d1d1f; }
.preview-json details { margin-left: 20px; }
.preview-json summary { cursor: pointer; margin-left: -20px; }
.preview-json summary:hover { color: #007aff; }
.preview-text {
background: #f5f5f7;
padding: 16px;
border-radius: 8px;
font-family: 'SF Mono', Menlo, monospace;
font-size: 13px;
white-space: pre-wrap;
}
.preview-xml { color: #1d1d1f; }
.preview-xml .tag { color: #aa0d91; }
.preview-xml .attr { color: #007aff; }
.preview-xml .value { color: #c41a16; }
</style>
</head>
<body>
<div class="container">
<header>
<h1>API Explorer</h1>
<div class="header-links">
<a href="/docs">Swagger Docs</a>
<a href="/api">API Info</a>
</div>
</header>
<div class="request-bar">
<select class="method-select" id="method">
<option value="GET">GET</option>
<option value="POST">POST</option>
<option value="PUT">PUT</option>
<option value="PATCH">PATCH</option>
<option value="DELETE">DELETE</option>
</select>
<input type="text" class="url-input" id="url" placeholder="/hello" value="/hello">
<button class="send-btn" id="send">Send</button>
<div class="copy-dropdown">
<button class="copy-btn" id="copy-toggle">Copy as...</button>
<div class="copy-menu" id="copy-menu">
<button class="copy-option" data-format="curl">
<div class="copy-option-label">cURL</div>
<div class="copy-option-desc">Command line</div>
</button>
<button class="copy-option" data-format="python">
<div class="copy-option-label">Python requests</div>
<div class="copy-option-desc">requests library</div>
</button>
<button class="copy-option" data-format="fetch">
<div class="copy-option-label">JavaScript fetch</div>
<div class="copy-option-desc">Browser/Node.js</div>
</button>
<button class="copy-option" data-format="httpie">
<div class="copy-option-label">HTTPie</div>
<div class="copy-option-desc">CLI for humans</div>
</button>
</div>
</div>
</div>
<div class="toast" id="toast">Copied to clipboard!</div>
<div class="main-layout">
<!-- Request Panel -->
<div class="panel">
<div class="tabs" id="request-tabs">
<button class="tab active" data-tab="req-body">Body</button>
<button class="tab" data-tab="req-form">Form</button>
<button class="tab" data-tab="req-file">File</button>
<button class="tab" data-tab="req-headers">Headers</button>
</div>
<div class="panel-body">
<div class="tab-content active" id="req-body-tab">
<textarea id="body" placeholder='{"name": "Laptop", "price": 999.99, "quantity": 5}'></textarea>
</div>
<div class="tab-content" id="req-form-tab">
<div id="form-fields">
<div class="form-row">
<div class="form-group">
<label class="form-label">Field Name</label>
<input type="text" class="form-input form-field-name" placeholder="username">
</div>
<div class="form-group">
<label class="form-label">Value</label>
<input type="text" class="form-input form-field-value" placeholder="john_doe">
</div>
</div>
</div>
<button type="button" id="add-form-field" style="margin-top:8px;padding:6px 12px;background:#f5f5f7;border:1px solid #d2d2d7;border-radius:6px;font-size:12px;cursor:pointer;">+ Add Field</button>
<div style="margin-top:12px;font-size:11px;color:#86868b;">
Content-Type: application/x-www-form-urlencoded
</div>
</div>
<div class="tab-content" id="req-file-tab">
<div class="file-upload-area" id="file-drop-area">
<div class="file-upload-icon">📁</div>
<div class="file-upload-text">
<strong>Click to upload</strong> or drag and drop<br>
<span style="font-size:11px;">Any file type supported</span>
</div>
<input type="file" id="file-input" class="file-input" multiple>
</div>
<div class="file-list" id="file-list"></div>
<div id="file-metadata" style="margin-top:12px;display:none;">
<div class="form-group">
<label class="form-label">Title (optional)</label>
<input type="text" class="form-input" id="file-title" placeholder="My Document">
</div>
<div class="form-group">
<label class="form-label">Description (optional)</label>
<input type="text" class="form-input" id="file-description" placeholder="A description of the file">
</div>
</div>
<div style="margin-top:12px;font-size:11px;color:#86868b;">
Content-Type: multipart/form-data
</div>
</div>
<div class="tab-content" id="req-headers-tab">
<textarea id="headers" placeholder="Content-Type: application/json">Content-Type: application/json</textarea>
</div>
</div>
</div>
<!-- Response Panel -->
<div class="panel">
<div class="status-bar">
<div class="status-item">Status<span class="status-value" id="status">-</span></div>
<div class="status-item">Time<span class="status-value" id="time">-</span></div>
<div class="status-item">Size<span class="status-value" id="size">-</span></div>
<div class="status-item">Type<span class="status-value" id="content-type">-</span></div>
</div>
<div class="tabs" id="response-tabs">
<button class="tab active" data-tab="res-body">Body</button>
<button class="tab" data-tab="res-preview">Preview</button>
<button class="tab" data-tab="res-headers">Headers</button>
<button class="tab" data-tab="res-raw">Raw</button>
</div>
<div class="tab-content active" id="res-body-tab">
<div class="response-content" id="response">Send a request to see the response</div>
</div>
<div class="tab-content" id="res-preview-tab">
<div class="preview-container" id="preview-container">
<div class="preview-placeholder">Send a request to see preview</div>
</div>
</div>
<div class="tab-content" id="res-headers-tab">
<div class="panel-body">
<div class="headers-list" id="response-headers"></div>
</div>
</div>
<div class="tab-content" id="res-raw-tab">
<div class="response-content" id="response-raw"></div>
</div>
</div>
</div>
<div class="examples-section">
<h3>Examples</h3>
<div class="examples-grid" id="examples"></div>
</div>
<!-- Code Snippet Panel -->
<div class="code-panel" id="code-panel">
<div class="code-panel-header">
<span class="code-panel-title" id="code-panel-title">cURL</span>
<button class="code-panel-close" id="code-panel-close">&times;</button>
</div>
<div class="code-panel-content" id="code-panel-content"></div>
<div class="code-panel-actions">
<button class="code-copy-btn" id="code-copy-btn">Copy to Clipboard</button>
</div>
</div>
</div>
<script>
const examples = [
{ method: 'GET', url: '/hello', title: 'Hello World' },
{ method: 'GET', url: '/time', title: 'Server Time' },
{ method: 'GET', url: '/greet?name=Student', title: 'Query Param' },
{ method: 'GET', url: '/greet/Alice', title: 'Path Param' },
{ method: 'GET', url: '/items', title: 'List Items' },
{ method: 'GET', url: '/items/1', title: 'Get Item' },
{ method: 'POST', url: '/items', title: 'Create Item', body: '{"name": "Laptop", "price": 999.99, "quantity": 5}' },
{ method: 'POST', url: '/echo', title: 'Echo JSON', body: '{"message": "Hello!", "count": 42}' },
{ method: 'PUT', url: '/items/1', title: 'Replace Item', body: '{"name": "Green Apple", "price": 2.50, "quantity": 200}' },
{ method: 'PATCH', url: '/items/1', title: 'Partial Update', body: '{"price": 1.99}' },
{ method: 'DELETE', url: '/items/3', title: 'Delete Item' },
{ method: 'POST', url: '/form/login', title: 'Form Login', formData: {username: 'john_doe', password: 'secret123', remember_me: 'true'} },
{ method: 'POST', url: '/form/contact', title: 'Contact Form', formData: {name: 'Alice', email: 'alice@example.com', subject: 'Hello', message: 'Nice API!'} },
{ method: 'POST', url: '/upload/file', title: 'File Upload', isFileUpload: true },
{ method: 'GET', url: '/upload/files', title: 'List Uploads' },
{ method: 'GET', url: '/format/json', title: 'JSON Format' },
{ method: 'GET', url: '/format/xml', title: 'XML Format' },
{ method: 'GET', url: '/format/html', title: 'HTML Format' },
{ method: 'GET', url: '/format/csv', title: 'CSV Format' },
{ method: 'GET', url: '/headers', title: 'View Headers' },
{ method: 'GET', url: '/status/404', title: 'Error 404' },
];
// Track selected files
let selectedFiles = [];
// Render examples
const examplesContainer = document.getElementById('examples');
examples.forEach(ex => {
const btn = document.createElement('button');
btn.className = 'example-btn';
btn.innerHTML = `
<span class="method-badge method-${ex.method}">${ex.method}</span>
<div class="example-info">
<div class="example-title">${ex.title}</div>
<div class="example-url">${ex.url}</div>
</div>
`;
btn.onclick = () => {
document.getElementById('method').value = ex.method;
document.getElementById('url').value = ex.url;
document.getElementById('body').value = ex.body || '';
// Handle form data examples
if (ex.formData) {
switchToTab('request-tabs', 'req-form');
populateFormFields(ex.formData);
} else if (ex.isFileUpload) {
switchToTab('request-tabs', 'req-file');
} else {
switchToTab('request-tabs', 'req-body');
}
};
examplesContainer.appendChild(btn);
});
function switchToTab(tabsId, tabName) {
const container = document.getElementById(tabsId);
container.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
const tab = container.querySelector(`[data-tab="${tabName}"]`);
if (tab) {
tab.classList.add('active');
const panel = container.closest('.panel') || document;
panel.querySelectorAll(':scope > .tab-content, :scope > .panel-body > .tab-content').forEach(c => c.classList.remove('active'));
document.getElementById(tabName + '-tab').classList.add('active');
}
}
function populateFormFields(data) {
const container = document.getElementById('form-fields');
container.innerHTML = '';
Object.entries(data).forEach(([key, value], i) => {
const row = document.createElement('div');
row.className = 'form-row';
row.innerHTML = `
<div class="form-group">
<label class="form-label">${i === 0 ? 'Field Name' : ''}</label>
<input type="text" class="form-input form-field-name" value="${key}" placeholder="field_name">
</div>
<div class="form-group">
<label class="form-label">${i === 0 ? 'Value' : ''}</label>
<input type="text" class="form-input form-field-value" value="${value}" placeholder="value">
</div>
`;
container.appendChild(row);
});
}
// Tab switching
function setupTabs(containerId) {
const container = document.getElementById(containerId);
container.querySelectorAll('.tab').forEach(tab => {
tab.onclick = () => {
container.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
tab.classList.add('active');
const panel = container.closest('.panel') || document;
panel.querySelectorAll(':scope > .tab-content, :scope > .panel-body > .tab-content').forEach(c => c.classList.remove('active'));
document.getElementById(tab.dataset.tab + '-tab').classList.add('active');
};
});
}
setupTabs('request-tabs');
setupTabs('response-tabs');
// JSON highlighting
function highlightJSON(str) {
return str
.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
.replace(/"([^"]+)":/g, '<span class="json-key">"$1"</span>:')
.replace(/: "([^"]*)"/g, ': <span class="json-string">"$1"</span>')
.replace(/: (-?\\d+\\.?\\d*)/g, ': <span class="json-number">$1</span>')
.replace(/: (true|false)/g, ': <span class="json-boolean">$1</span>')
.replace(/: (null)/g, ': <span class="json-null">$1</span>');
}
// Get active request type
function getActiveRequestType() {
const formTab = document.getElementById('req-form-tab');
const fileTab = document.getElementById('req-file-tab');
if (formTab.classList.contains('active')) return 'form';
if (fileTab.classList.contains('active')) return 'file';
return 'body';
}
// Get form data from the form fields
function getFormFieldsData() {
const data = {};
const names = document.querySelectorAll('.form-field-name');
const values = document.querySelectorAll('.form-field-value');
names.forEach((nameInput, i) => {
const name = nameInput.value.trim();
const value = values[i]?.value || '';
if (name) data[name] = value;
});
return data;
}
// Send request
async function sendRequest() {
const method = document.getElementById('method').value;
const url = document.getElementById('url').value;
const body = document.getElementById('body').value;
const headersText = document.getElementById('headers').value;
const requestType = getActiveRequestType();
const headers = {};
headersText.split('\\n').forEach(line => {
const [key, ...vals] = line.split(':');
if (key && vals.length) headers[key.trim()] = vals.join(':').trim();
});
const opts = { method, headers };
if (['POST', 'PUT', 'PATCH'].includes(method)) {
if (requestType === 'form') {
// Form URL-encoded data
const formData = getFormFieldsData();
opts.body = new URLSearchParams(formData).toString();
opts.headers['Content-Type'] = 'application/x-www-form-urlencoded';
} else if (requestType === 'file' && selectedFiles.length > 0) {
// Multipart form data for file upload
const formData = new FormData();
if (selectedFiles.length === 1) {
formData.append('file', selectedFiles[0]);
} else {
selectedFiles.forEach(f => formData.append('files', f));
}
// Add optional metadata
const title = document.getElementById('file-title')?.value;
const description = document.getElementById('file-description')?.value;
if (title) formData.append('title', title);
if (description) formData.append('description', description);
opts.body = formData;
// Don't set Content-Type for FormData - browser sets it with boundary
delete opts.headers['Content-Type'];
} else if (body) {
opts.body = body;
}
}
document.getElementById('send').disabled = true;
document.getElementById('send').textContent = 'Sending...';
const start = performance.now();
try {
const res = await fetch(url, opts);
const elapsed = Math.round(performance.now() - start);
const contentType = res.headers.get('content-type') || '';
const isBinary = contentType.includes('image/') || contentType.includes('octet-stream');
// Status
const statusEl = document.getElementById('status');
statusEl.textContent = res.status + ' ' + res.statusText;
statusEl.className = 'status-value ' + (res.ok ? 'status-ok' : (res.status >= 500 ? 'status-err' : 'status-warn'));
// Response headers
const headersHtml = [];
res.headers.forEach((value, name) => {
headersHtml.push(`<div class="header-row"><span class="header-name">${name}</span><span class="header-value">${value}</span></div>`);
});
document.getElementById('response-headers').innerHTML = headersHtml.join('') || '<div style="color:#86868b">No headers</div>';
if (isBinary) {
// Handle binary data
const blob = await res.blob();
document.getElementById('time').textContent = elapsed + 'ms';
document.getElementById('size').textContent = blob.size > 1024 ? (blob.size / 1024).toFixed(1) + ' KB' : blob.size + ' B';
document.getElementById('content-type').textContent = contentType.split(';')[0] || '-';
document.getElementById('response-raw').textContent = `[Binary data: ${blob.size} bytes]`;
document.getElementById('response').innerHTML = `<span style="color:#86868b">[Binary data: ${blob.size} bytes, type: ${contentType}]</span>`;
// Preview binary
renderBinaryPreview(blob, contentType, url);
} else {
// Handle text data
const text = await res.text();
document.getElementById('time').textContent = elapsed + 'ms';
document.getElementById('size').textContent = text.length > 1024 ? (text.length / 1024).toFixed(1) + ' KB' : text.length + ' B';
document.getElementById('content-type').textContent = contentType.split(';')[0] || '-';
// Raw response
document.getElementById('response-raw').textContent = text;
// Formatted response
let display;
try {
const json = JSON.parse(text);
display = highlightJSON(JSON.stringify(json, null, 2));
} catch {
display = text.replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
document.getElementById('response').innerHTML = display;
// Preview
renderPreview(text, contentType);
}
} catch (err) {
document.getElementById('status').textContent = 'Error';
document.getElementById('status').className = 'status-value status-err';
document.getElementById('response').textContent = 'Request failed: ' + err.message;
document.getElementById('response-headers').innerHTML = '';
document.getElementById('response-raw').textContent = '';
}
document.getElementById('send').disabled = false;
document.getElementById('send').textContent = 'Send';
}
document.getElementById('send').onclick = sendRequest;
document.getElementById('url').onkeydown = (e) => { if (e.key === 'Enter') sendRequest(); };
sendRequest();
// Preview rendering for binary content
function renderBinaryPreview(blob, contentType, url) {
const container = document.getElementById('preview-container');
const ct = contentType.toLowerCase();
if (ct.includes('image/')) {
const blobUrl = URL.createObjectURL(blob);
container.innerHTML = `<img class="preview-image" src="${blobUrl}" alt="Image preview" onload="URL.revokeObjectURL(this.src)">`;
} else {
// Generic binary
container.innerHTML = `<div class="preview-text" style="text-align:center;padding:40px;">
<div style="font-size:32px;margin-bottom:12px;">Binary Data</div>
<div style="color:#86868b;">${blob.size} bytes</div>
<div style="color:#86868b;">${contentType}</div>
<a href="${url}" download style="display:inline-block;margin-top:16px;padding:8px 16px;background:#007aff;color:#fff;border-radius:6px;text-decoration:none;">Download</a>
</div>`;
}
}
// Preview rendering for text content
function renderPreview(text, contentType) {
const container = document.getElementById('preview-container');
const ct = contentType.toLowerCase();
if (ct.includes('text/html')) {
// Render HTML in sandboxed iframe
const iframe = document.createElement('iframe');
iframe.className = 'preview-iframe';
iframe.sandbox = 'allow-same-origin';
iframe.srcdoc = text;
container.innerHTML = '';
container.appendChild(iframe);
} else if (ct.includes('application/json') || ct.includes('json')) {
// Interactive JSON tree
try {
const json = JSON.parse(text);
container.innerHTML = '<div class="preview-json">' + renderJSONTree(json) + '</div>';
} catch {
container.innerHTML = '<div class="preview-text">' + text.replace(/</g, '&lt;') + '</div>';
}
} else if (ct.includes('xml')) {
// Syntax highlighted XML
const highlighted = text
.replace(/</g, '&lt;').replace(/>/g, '&gt;')
.replace(/&lt;(\\/?[\\w:-]+)/g, '&lt;<span class="tag">$1</span>')
.replace(/(\\w+)=("[^"]*")/g, '<span class="attr">$1</span>=<span class="value">$2</span>');
container.innerHTML = '<pre class="preview-text preview-xml">' + highlighted + '</pre>';
} else if (ct.includes('csv')) {
// Render CSV as table
container.innerHTML = renderCSVTable(text);
} else if (ct.includes('markdown')) {
// Basic markdown rendering
container.innerHTML = '<div class="preview-text">' + renderBasicMarkdown(text) + '</div>';
} else {
// Plain text
container.innerHTML = '<pre class="preview-text">' + text.replace(/</g, '&lt;').replace(/>/g, '&gt;') + '</pre>';
}
}
function renderJSONTree(obj, level = 0) {
if (obj === null) return '<span class="null">null</span>';
if (typeof obj === 'boolean') return '<span class="boolean">' + obj + '</span>';
if (typeof obj === 'number') return '<span class="number">' + obj + '</span>';
if (typeof obj === 'string') return '<span class="string">"' + obj.replace(/</g, '&lt;') + '"</span>';
if (Array.isArray(obj)) {
if (obj.length === 0) return '<span class="bracket">[]</span>';
let html = '<span class="bracket">[</span>';
if (level < 2) {
html += '<br>' + obj.map((v, i) =>
' '.repeat(level + 1) + renderJSONTree(v, level + 1) + (i < obj.length - 1 ? ',' : '')
).join('<br>') + '<br>' + ' '.repeat(level);
} else {
html += obj.map((v, i) => renderJSONTree(v, level + 1) + (i < obj.length - 1 ? ', ' : '')).join('');
}
html += '<span class="bracket">]</span>';
return html;
}
if (typeof obj === 'object') {
const keys = Object.keys(obj);
if (keys.length === 0) return '<span class="bracket">{}</span>';
let html = '<span class="bracket">{</span>';
if (level < 2) {
html += '<br>' + keys.map((k, i) =>
' '.repeat(level + 1) + '<span class="key">"' + k + '"</span>: ' + renderJSONTree(obj[k], level + 1) + (i < keys.length - 1 ? ',' : '')
).join('<br>') + '<br>' + ' '.repeat(level);
} else {
html += keys.map((k, i) =>
'<span class="key">"' + k + '"</span>: ' + renderJSONTree(obj[k], level + 1) + (i < keys.length - 1 ? ', ' : '')
).join('');
}
html += '<span class="bracket">}</span>';
return html;
}
return String(obj);
}
function renderCSVTable(csv) {
const lines = csv.trim().split('\\n');
if (lines.length === 0) return '<div class="preview-text">Empty CSV</div>';
let html = '<table style="width:100%;border-collapse:collapse;font-size:13px;">';
lines.forEach((line, i) => {
const cells = line.split(',');
const tag = i === 0 ? 'th' : 'td';
const style = 'padding:8px 12px;border:1px solid #d2d2d7;text-align:left;' + (i === 0 ? 'background:#f5f5f7;font-weight:600;' : '');
html += '<tr>' + cells.map(c => `<${tag} style="${style}">${c.trim()}</${tag}>`).join('') + '</tr>';
});
html += '</table>';
return html;
}
function renderBasicMarkdown(md) {
return md
.replace(/^### (.+)$/gm, '<h3 style="margin:12px 0 8px;font-size:16px;">$1</h3>')
.replace(/^## (.+)$/gm, '<h2 style="margin:16px 0 8px;font-size:18px;">$1</h2>')
.replace(/^# (.+)$/gm, '<h1 style="margin:20px 0 12px;font-size:22px;">$1</h1>')
.replace(/\\*\\*(.+?)\\*\\*/g, '<strong>$1</strong>')
.replace(/\\*(.+?)\\*/g, '<em>$1</em>')
.replace(/`([^`]+)`/g, '<code style="background:#e8e8e8;padding:2px 6px;border-radius:4px;">$1</code>')
.replace(/^- (.+)$/gm, '<li style="margin:4px 0;">$1</li>')
.replace(/\\n/g, '<br>');
}
// Copy as... functionality
const copyToggle = document.getElementById('copy-toggle');
const copyMenu = document.getElementById('copy-menu');
const toast = document.getElementById('toast');
copyToggle.onclick = (e) => {
e.stopPropagation();
copyMenu.classList.toggle('show');
};
document.addEventListener('click', () => copyMenu.classList.remove('show'));
function getBaseUrl() {
return window.location.origin;
}
function generateCode(format) {
const method = document.getElementById('method').value;
const url = document.getElementById('url').value;
const body = document.getElementById('body').value;
const headersText = document.getElementById('headers').value;
const fullUrl = getBaseUrl() + url;
const headers = {};
headersText.split('\\n').forEach(line => {
const [key, ...vals] = line.split(':');
if (key && vals.length) headers[key.trim()] = vals.join(':').trim();
});
const hasBody = ['POST', 'PUT', 'PATCH'].includes(method) && body;
switch (format) {
case 'curl': {
let cmd = `curl -X ${method} "${fullUrl}"`;
for (const [k, v] of Object.entries(headers)) {
cmd += ` \\\\\\n -H "${k}: ${v}"`;
}
if (hasBody) {
cmd += ` \\\\\\n -d '${body.replace(/'/g, "\\'")}'`;
}
return cmd;
}
case 'python': {
let code = `import requests\\n\\n`;
code += `url = "${fullUrl}"\\n`;
if (Object.keys(headers).length) {
code += `headers = ${JSON.stringify(headers, null, 4)}\\n`;
}
if (hasBody) {
code += `data = ${body}\\n\\n`;
code += `response = requests.${method.toLowerCase()}(url`;
if (Object.keys(headers).length) code += `, headers=headers`;
code += `, json=data)`;
} else {
code += `\\nresponse = requests.${method.toLowerCase()}(url`;
if (Object.keys(headers).length) code += `, headers=headers`;
code += `)`;
}
code += `\\nprint(response.status_code)\\nprint(response.json())`;
return code;
}
case 'fetch': {
let code = `fetch("${fullUrl}", {\\n`;
code += ` method: "${method}",\\n`;
if (Object.keys(headers).length) {
code += ` headers: ${JSON.stringify(headers, null, 4).replace(/\\n/g, '\\n ')},\\n`;
}
if (hasBody) {
code += ` body: JSON.stringify(${body})\\n`;
}
code += `})\\n`;
code += `.then(res => res.json())\\n`;
code += `.then(data => console.log(data))\\n`;
code += `.catch(err => console.error(err));`;
return code;
}
case 'httpie': {
let cmd = `http ${method} "${fullUrl}"`;
for (const [k, v] of Object.entries(headers)) {
cmd += ` "${k}:${v}"`;
}
if (hasBody) {
try {
const jsonBody = JSON.parse(body);
for (const [k, v] of Object.entries(jsonBody)) {
const val = typeof v === 'string' ? `="${v}"` : `:=${JSON.stringify(v)}`;
cmd += ` ${k}${val}`;
}
} catch {
cmd += ` --raw '${body}'`;
}
}
return cmd;
}
}
}
function showToast(msg) {
toast.textContent = msg;
toast.classList.add('show');
setTimeout(() => toast.classList.remove('show'), 2000);
}
document.querySelectorAll('.copy-option').forEach(btn => {
btn.onclick = async (e) => {
e.stopPropagation();
const format = btn.dataset.format;
const code = generateCode(format);
const formattedCode = code.replace(/\\\\n/g, '\\n');
// Show in code panel
showCodePanel(format, formattedCode);
try {
await navigator.clipboard.writeText(formattedCode);
showToast(`Copied as ${format.toUpperCase()}!`);
} catch {
showToast('Failed to copy');
}
copyMenu.classList.remove('show');
};
});
// Code panel functionality
const codePanel = document.getElementById('code-panel');
const codePanelTitle = document.getElementById('code-panel-title');
const codePanelContent = document.getElementById('code-panel-content');
const codePanelClose = document.getElementById('code-panel-close');
const codeCopyBtn = document.getElementById('code-copy-btn');
function showCodePanel(format, code) {
const titles = {
curl: 'cURL Command',
python: 'Python (requests)',
fetch: 'JavaScript (fetch)',
httpie: 'HTTPie Command'
};
codePanelTitle.textContent = titles[format] || format.toUpperCase();
codePanelContent.textContent = code;
codePanel.classList.add('show');
}
codePanelClose.onclick = () => codePanel.classList.remove('show');
codeCopyBtn.onclick = async () => {
try {
await navigator.clipboard.writeText(codePanelContent.textContent);
showToast('Copied to clipboard!');
} catch {
showToast('Failed to copy');
}
};
// File upload handling
const fileDropArea = document.getElementById('file-drop-area');
const fileInput = document.getElementById('file-input');
const fileList = document.getElementById('file-list');
const fileMetadata = document.getElementById('file-metadata');
fileDropArea.onclick = () => fileInput.click();
fileDropArea.ondragover = (e) => {
e.preventDefault();
fileDropArea.classList.add('dragover');
};
fileDropArea.ondragleave = () => {
fileDropArea.classList.remove('dragover');
};
fileDropArea.ondrop = (e) => {
e.preventDefault();
fileDropArea.classList.remove('dragover');
handleFiles(e.dataTransfer.files);
};
fileInput.onchange = (e) => {
handleFiles(e.target.files);
};
function handleFiles(files) {
selectedFiles = Array.from(files);
renderFileList();
}
function renderFileList() {
if (selectedFiles.length === 0) {
fileList.innerHTML = '';
fileMetadata.style.display = 'none';
return;
}
fileList.innerHTML = selectedFiles.map((file, i) => `
<div class="file-item">
<div>
<span class="file-item-name">${file.name}</span>
<span class="file-item-size">${formatFileSize(file.size)}</span>
</div>
<button class="file-item-remove" onclick="removeFile(${i})">&times;</button>
</div>
`).join('');
fileMetadata.style.display = 'block';
}
function formatFileSize(bytes) {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
}
window.removeFile = function(index) {
selectedFiles.splice(index, 1);
renderFileList();
};
// Add form field button
document.getElementById('add-form-field').onclick = () => {
const container = document.getElementById('form-fields');
const row = document.createElement('div');
row.className = 'form-row';
row.innerHTML = `
<div class="form-group">
<input type="text" class="form-input form-field-name" placeholder="field_name">
</div>
<div class="form-group">
<input type="text" class="form-input form-field-value" placeholder="value">
</div>
`;
container.appendChild(row);
};
</script>
</body>
</html>
"""
# ============================================================================
# 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 """
<!DOCTYPE html>
<html>
<head>
<title>HTML Response</title>
<style>
body { font-family: Arial, sans-serif; padding: 20px; }
.card { border: 1px solid #ddd; padding: 15px; border-radius: 8px; max-width: 300px; }
h1 { color: #333; }
</style>
</head>
<body>
<h1>HTML Response Example</h1>
<div class="card">
<h2>User Info</h2>
<p><strong>Name:</strong> Alice</p>
<p><strong>Age:</strong> 30</p>
<p><strong>City:</strong> Mumbai</p>
</div>
<p><em>Content-Type: text/html</em></p>
</body>
</html>
"""
@app.get("/format/xml", tags=["Response Formats"])
def format_xml():
"""Returns data as XML"""
xml_content = """<?xml version="1.0" encoding="UTF-8"?>
<response>
<format>XML</format>
<content_type>application/xml</content_type>
<data>
<user>
<name>Alice</name>
<age>30</age>
<city>Mumbai</city>
</user>
</data>
</response>"""
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)