Concepts 🚀 Project 🧮 Labs
📚 Learn / Mini Projects / Form + Fetch + FastAPI Backend

Form + Fetch + FastAPI Backend

A login form that POSTs to FastAPI, a button that GETs users, and a live clock — the exact pattern behind every auth flow.

🚀 Mini Project · 3 files

Project files

📄 index.html html
<!DOCTYPE html>
<html>
<head><title>Sign In</title></head>
<body>
  <form method='post' action='http://127.0.0.1:8000/signin' enctype='multipart/form-data'>
    <input type='text' name='username' placeholder='Name'>
    <input type='number' name='id' placeholder='ID'>
    <input type='file' name='file'>
    <button id='login'>Login</button>
    <a href='http://127.0.0.1:8000/get-users'>Retrieve Users</a>
  </form>
  <h5 id='sample'>User data will appear here</h5>
  <button id='g1'>Click To Get</button>
  <input id='g2' placeholder='type something'>
  <h3 id='g3'>YOUR INPUT</h3>
  <h3 id='clock'>Clock</h3>

  <script>
    // Live clock — update every second
    setInterval(() => {
      document.getElementById('clock').textContent = new Date().toLocaleTimeString();
    }, 1000);

    // Live input mirror — fires on every keystroke
    document.getElementById('g2').addEventListener('input', (e) => {
      document.getElementById('g3').textContent = e.target.value;
    });

    // Fetch users from FastAPI
    document.getElementById('g1').addEventListener('click', async (e) => {
      e.preventDefault();
      const res = await fetch('http://127.0.0.1:8000/get-users');
      const data = await res.json();
      const u = data[0];
      document.getElementById('sample').innerHTML =
        '<p>' + u.username + ' : ' + u.id + '</p>';
    });
  </script>
</body>
</html>
📄 main.py python
from fastapi import FastAPI, UploadFile, File, Form
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
import uvicorn, shutil, os

app = FastAPI()

# Allow the browser to call this API from a different origin (file://)
app.add_middleware(
    CORSMiddleware,
    allow_origins=['*'],
    allow_methods=['*'],
    allow_headers=['*'],
)

# In-memory user store (use a DB in real apps)
USERS = [
    {'id': 1, 'username': 'Ada'},
    {'id': 2, 'username': 'Linus'},
]

@app.get('/get-users')
async def get_users():
    return USERS

@app.post('/signin')
async def signin(
    username: str = Form(...),
    id: int = Form(...),
    file: UploadFile = File(...),
):
    # Save uploaded file
    os.makedirs('uploads', exist_ok=True)
    with open(f'uploads/{file.filename}', 'wb') as f:
        shutil.copyfileobj(file.file, f)
    USERS.append({'id': id, 'username': username})
    return {'status': 'ok', 'user': username, 'file': file.filename}

if __name__ == '__main__':
    uvicorn.run(app, host='0.0.0.0', port=8000)
📄 run.sh bash
# Install FastAPI + uvicorn
pip install fastapi uvicorn python-multipart

# Run the server
python main.py

# Open index.html in a browser (double-click the file)
# Try: type a name in the form, upload a file, click Login
# Then click 'Click To Get' to see the user list

How it works

This is the foundational full-stack pattern. Step by step: (1) The HTML form collects username + id + file. method='post' sends it to /signin. enctype='multipart/form-data' is REQUIRED because the form has a file input — without it, the upload silently fails. (2) FastAPI's @app.post('/signin') receives the form fields via Form(...) and the file via UploadFile. shutil.copyfileobj streams the file to disk without loading it all into memory. (3) The 'Click To Get' button calls fetch('http://127.0.0.1:8000/get-users') which returns JSON. await res.json() parses it. (4) setInterval updates the clock every 1000ms. (5) The 'input' event listener mirrors the text field into the <h3> on every keystroke. CORS middleware is needed because the browser loads index.html from file:// but calls the API at http://127.0.0.1:8000 — different origins. Without CORS, the browser blocks the request.

⚡ Open in Notebook
🔍 Where this is used in OpenBenchML OpenBenchML's login.html + auth.py use this exact pattern: form POSTs to /login, FastAPI validates + sets a JWT cookie, the next page loads.