This repo contains both the Flask API and a small HTML form that submits JSON to it.
survey-intake-case-study-part1/
├─ app.py # Flask API (POST /v1/survey)
├─ models.py # Pydantic v1 schemas (validation)
├─ storage.py # Append-only JSON Lines helper
├─ requirements.txt
├─ frontend/
│ ├─ index.html # Survey form (vanilla HTML + fetch)
│ └─ styles.css
├─ data/ # Will contain survey.ndjson after submissions
└─ tests/
└─ test_api.py # Minimal API tests
-
Clone & enter
git clone <your-fork-url> survey-intake-case-study-part1 cd survey-intake-case-study-part1
-
Create a virtualenv & install deps
python -m venv .venv && source .venv/bin/activate pip install -r requirements.txt
-
Run tests (optional but recommended)
pytest -q
-
Start the API
python app.py # -> listening on http://127.0.0.1:5000 -
Open the HTML form
- EITHER open
frontend/index.htmldirectly in your browser (double-click the file), - OR run a tiny static server from the
frontend/folder:cd frontend python -m http.server 8000 # visit http://127.0.0.1:8000
- EITHER open
Why does the form work if served separately?
We enabled permissive CORS on the Flask app so the static page can POST across origins during development. In production, restrict CORS to approved origins.
Fill out the form and click Submit. On success you’ll see:
- A 201 response on the network tab and
- A green success message.
Check the stored data:
tail -n1 data/survey.ndjson | jqYou should see your submission with server-added fields:
{"name":"Ava","email":"ava@example.com","age":22,"consent":true,"rating":4,"comments":"Loved it!","source":"homepage","received_at":"2025-01-01T00:00:00Z","ip":"127.0.0.1"}<!DOCTYPE html>and<html lang="en">: HTML5 doc & language hint.<meta charset="UTF-8">: UTF‑8 so emojis/accents work.<meta name="viewport" ...>: mobile-friendly scaling.<link rel="stylesheet" href="./styles.css">: styles.<form id="survey-form" novalidate>: we handle validation & messages ourselves (JS + server).- Inputs:
name(text),email(email),age(number),consent(checkbox),rating(select),comments(textarea).- Hidden
sourcefield (defaults to"homepage").
<section id="result" ...>: where we display success/error messages.- Script:
form.addEventListener("submit", ...): intercept submit,preventDefault().FormData(form)→ build a JS objectpayload.- Type coercion:
Number(...)forage&rating,checkbox==="on"→ boolean. fetch("http://127.0.0.1:5000/v1/survey", {method:"POST", headers:{"Content-Type":"application/json"}, body: JSON.stringify(payload)})- If
res.ok→ show green message andform.reset(). - Else → show detailed error (422 validation errors include field-level info from Pydantic).
- Finally → re-enable the submit button.
app = Flask(__name__)instantiates the app (import name used for defaults).CORS(app, resources={r"/v1/*": {"origins": "*"}})enables cross-origin fetches for dev.@app.post("/v1/survey")handles JSON POST.request.get_json(silent=True): returnsNoneif not valid JSON → 400.SurveySubmission(**payload): Pydantic v1 validation → raisesValidationError→ 422 with details.- Enrich with
received_at(UTC) andip. append_json_line(record.dict())writes todata/survey.ndjson.- On success → 201 with
{"status":"ok"}.
- JSON Lines (NDJSON) is append-only and easy to parse later (pandas, Spark).
- Keep payloads small (<16KB). In Part 2, we’ll add a request-size guard and export/analytics.
- CORS/Network error: Is
python app.pyrunning? Is the URL exactlyhttp://127.0.0.1:5000/v1/survey? - Validation 422: Read
detailarray — each item includes the field path and message. - Permission denied writing file: Ensure you run from repo root so
data/exists and is writable.
GET /v1/survey/exportto stream NDJSON- Simple analytics: average rating, counts by source
- CSV export for Excel users