Spaces:
Running
Running
| """ | |
| 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 | |
| # ============================================================================ | |
| 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}"], | |
| } | |
| } | |
| 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">×</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, '&').replace(/</g, '<').replace(/>/g, '>') | |
| .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, '<').replace(/>/g, '>'); | |
| } | |
| 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, '<') + '</div>'; | |
| } | |
| } else if (ct.includes('xml')) { | |
| // Syntax highlighted XML | |
| const highlighted = text | |
| .replace(/</g, '<').replace(/>/g, '>') | |
| .replace(/<(\\/?[\\w:-]+)/g, '<<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, '<').replace(/>/g, '>') + '</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, '<') + '"</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})">×</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 | |
| # ============================================================================ | |
| def hello(): | |
| """Simplest GET endpoint - returns a greeting""" | |
| return {"message": "Hello, World!"} | |
| 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") | |
| } | |
| 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} | |
| 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} | |
| 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 | |
| # ============================================================================ | |
| def list_items(): | |
| """GET all items""" | |
| return {"items": list(items_db.values()), "count": len(items_db)} | |
| 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] | |
| 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} | |
| 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} | |
| 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} | |
| 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 | |
| # ============================================================================ | |
| 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" | |
| } | |
| 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 | |
| } | |
| 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 | |
| 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)" | |
| } | |
| 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() | |
| } | |
| 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}" | |
| } | |
| 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) | |
| } | |
| 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}" | |
| } | |
| 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)} | |
| 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"]}"' | |
| } | |
| ) | |
| 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 | |
| # ============================================================================ | |
| def format_json(): | |
| """Returns data as JSON (default)""" | |
| return { | |
| "format": "JSON", | |
| "content_type": "application/json", | |
| "data": {"name": "Alice", "age": 30, "city": "Mumbai"} | |
| } | |
| 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!""" | |
| 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> | |
| """ | |
| 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") | |
| 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"} | |
| ) | |
| 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") | |
| 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") | |
| 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") | |
| 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 | |
| # ============================================================================ | |
| 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)" | |
| } | |
| } | |
| 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) | |
| 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} | |
| ) | |
| 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) | |
| } | |
| 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 | |
| # ============================================================================ | |
| 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) | |