api-testing / app.py
Nipun's picture
Fix binary/image handling - use blob for preview
1b9bcd8
raw
history blame
54.5 kB
"""
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
from fastapi.responses import PlainTextResponse, HTMLResponse, JSONResponse
from pydantic import BaseModel, Field
from typing import Optional, List
from datetime import datetime
import json
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}"],
"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; }
/* 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-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-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>
</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: '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: '/format/yaml', title: 'YAML Format' },
{ method: 'GET', url: '/format/markdown', title: 'Markdown' },
{ method: 'GET', url: '/format/image', title: 'PNG Image' },
{ method: 'GET', url: '/headers', title: 'View Headers' },
{ method: 'GET', url: '/status/404', title: 'Error 404' },
{ method: 'GET', url: '/status/201', title: 'Status 201' },
];
// 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 || '';
};
examplesContainer.appendChild(btn);
});
// 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>');
}
// 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 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) && 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);
try {
await navigator.clipboard.writeText(code.replace(/\\\\n/g, '\\n'));
showToast(`Copied as ${format.toUpperCase()}!`);
} catch {
showToast('Failed to copy');
}
copyMenu.classList.remove('show');
};
});
</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")
}
# ============================================================================
# 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)