Codeskill

Learn to code, step by step

Mini project API + front end

This mini project builds a task list API in Flask and a simple HTML and JavaScript front end that consumes it. Data lives in a JSON file on disk – no database required. The goal is to wire up the pieces from earlier tutorials: REST-ish routes, validation, CORS or same-origin static files, and fetch from the browser.

What we are building

  • Flask serves /api/tasks – list, create, toggle done
  • Tasks persist in data/tasks.json
  • Static index.html and app.js render the list and a form
  • pytest covers the API with Flask’s test client

Project layout

task_board/
├── pyproject.toml
├── data/tasks.json
├── src/task_board/
│   ├── __init__.py
│   ├── storage.py
│   ├── routes.py
│   └── static/
│       ├── index.html
│       └── app.js
└── tests/
    └── test_api.py

JSON storage layer

import json
import tempfile
from pathlib import Path

class TaskStore:
    def __init__(self, path: Path) -> None:
        self.path = path
        self.path.parent.mkdir(parents=True, exist_ok=True)
        if not self.path.exists():
            self._write([])

    def _read(self) -> list[dict]:
        return json.loads(self.path.read_text(encoding="utf-8"))

    def _write(self, tasks: list[dict]) -> None:
        with tempfile.NamedTemporaryFile("w", delete=False, dir=self.path.parent) as tmp:
            json.dump(tasks, tmp, indent=2)
            tmp_path = Path(tmp.name)
        tmp_path.replace(self.path)

    def list(self) -> list[dict]:
        return self._read()

    def add(self, title: str) -> dict:
        tasks = self._read()
        new_id = max((t["id"] for t in tasks), default=0) + 1
        task = {"id": new_id, "title": title, "done": False}
        tasks.append(task)
        self._write(tasks)
        return task

    def toggle(self, task_id: int) -> dict | None:
        tasks = self._read()
        for task in tasks:
            if task["id"] == task_id:
                task["done"] = not task["done"]
                self._write(tasks)
                return task
        return None

Flask app and routes

from flask import Flask, abort, jsonify, request, send_from_directory
from pathlib import Path
from task_board.storage import TaskStore

def create_app(data_path: Path | None = None) -> Flask:
    app = Flask(__name__, static_folder="static")
    store = TaskStore(data_path or Path("data/tasks.json"))

    @app.get("/")
    def index():
        return send_from_directory(app.static_folder, "index.html")

    @app.get("/api/tasks")
    def list_tasks():
        return jsonify(store.list())

    @app.post("/api/tasks")
    def create_task():
        data = request.get_json(silent=True) or {}
        title = (data.get("title") or "").strip()
        if not title:
            abort(400, description="title is required")
        return jsonify(store.add(title)), 201

    @app.patch("/api/tasks/<int:task_id>")
    def toggle_task(task_id: int):
        task = store.toggle(task_id)
        if not task:
            abort(404)
        return jsonify(task)

    return app

Front-end fetch logic

The static files live beside the Python package. Same origin means no CORS faff for local development.

// static/app.js
// async function api(path, options = {}) {
//   const response = await fetch(path, {
//     headers: {"Content-Type": "application/json"},
//     ...options,
//   });
//   if (!response.ok) throw new Error(await response.text());
//   return response.status === 204 ? null : response.json();
// }
//
// async function render() {
//   const tasks = await api("/api/tasks");
//   const list = document.querySelector("#tasks");
//   list.innerHTML = tasks.map(t =>
//     `<li><label>
//       <input type="checkbox" data-id="${t.id}" ${t.done ? "checked" : ""}>
//       ${t.title}
//     </label></li>`
//   ).join("");
// }
//
// document.querySelector("#add-form").addEventListener("submit", async (e) => {
//   e.preventDefault();
//   const input = document.querySelector("#title");
//   await api("/api/tasks", {method: "POST", body: JSON.stringify({title: input.value})});
//   input.value = "";
//   render();
// });
//
// render();

Tests

import json
from pathlib import Path
from task_board import create_app

def test_create_and_list(tmp_path: Path):
    data_file = tmp_path / "tasks.json"
    app = create_app(data_file)
    client = app.test_client()

    response = client.post("/api/tasks", json={"title": "Write docs"})
    assert response.status_code == 201
    payload = response.get_json()
    assert payload["title"] == "Write docs"
    assert payload["done"] is False

    listed = client.get("/api/tasks").get_json()
    assert len(listed) == 1

Run it

# export FLASK_APP=task_board:create_app
# flask --app "task_board:create_app()" run --debug
# Open http://127.0.0.1:5000/

Extensions if you want them: filter by done status, delete tasks, or split the static front end onto a separate dev server with Flask-CORS. The core architecture – file-backed store, JSON API, thin browser client – stays the same.

PreviousJS front end + Python API