diff --git a/pygitweb/tasks_routes_test.py b/pygitweb/tasks_routes_test.py
new file mode 100644
index 0000000..22a41ed
--- /dev/null
+++ b/pygitweb/tasks_routes_test.py
@@ -0,0 +1,308 @@
+from __future__ import annotations
+
+from collections.abc import Generator
+from pathlib import Path
+from unittest.mock import patch
+
+import pygit2
+import pytest
+from fastapi import FastAPI
+from fastapi.testclient import TestClient
+from pygit2 import Oid
+
+from pygittools.tasks import BOARD_REF_PREFIX, TASK_REF_PREFIX, Board, Comment, Task
+from pygitweb.config import settings
+from pygitweb.tasks import EMPTY_TREE_OID, board_router, comment_router, task_router
+
+
+def _build_client() -> TestClient:
+	app = FastAPI()
+	app.include_router(board_router, prefix="/board")
+	app.include_router(task_router, prefix="/tasks")
+	app.include_router(comment_router, prefix="/comments")
+	return TestClient(app)
+
+
+def _create_initial_commit(repo: pygit2.Repository, repo_dir: Path) -> None:
+	(repo_dir / "README.md").write_text("seed\n", encoding="utf-8")
+	index = repo.index
+	index.add("README.md")
+	index.write()
+	tree = index.write_tree()
+	sig = pygit2.Signature("tester", "tester@example.com")
+	repo.create_commit("HEAD", sig, sig, "initial", tree, [])
+
+
+class TestTaskRoutes:
+	class TestReadonlyResponses:
+		@pytest.fixture(scope="class")
+		def seeded_env(
+			self, tmp_path_factory: pytest.TempPathFactory
+		) -> Generator[dict[str, str | TestClient], None, None]:
+			root: Path = tmp_path_factory.mktemp("tasks-routes-readonly")
+			repo_dir: Path = root / "demo"
+			repo = pygit2.init_repository(str(repo_dir), bare=False)
+			project: str = "demo"
+
+			board_ref: str = f"{BOARD_REF_PREFIX}Tasks"
+			board = Board(EMPTY_TREE_OID, board_ref, tagger="", description="Main board")
+
+			task_a_ref: str = f"{TASK_REF_PREFIX}task_a"
+			task_a = Task(
+				EMPTY_TREE_OID,
+				task_a_ref,
+				tagger="",
+				title="Alpha",
+				description="Alpha description",
+				status=Task.Status.TODO,
+				priority=Task.Priority.HIGH,
+				assignee="alice",
+			)
+			task_a_oid = task_a.write(repo)
+			comment_a = Comment(task_a_oid, "alice <alice@example.com>", "alpha note")
+			comment_a_oid = comment_a.write(repo)
+			task_a.comments = [str(comment_a_oid)]
+			task_a.update_message()
+			task_a_oid = task_a.write(repo)
+
+			task_b_ref: str = f"{TASK_REF_PREFIX}task_b"
+			task_b = Task(
+				EMPTY_TREE_OID,
+				task_b_ref,
+				tagger="",
+				title="Bravo",
+				description="",
+				status=Task.Status.IN_PROGRESS,
+				priority=Task.Priority.MEDIUM,
+				assignee=None,
+			)
+			task_b_oid = task_b.write(repo)
+
+			board.tasks = [str(task_a_oid), str(task_b_oid)]
+			board.update_message()
+			board.write(repo)
+
+			with (
+				patch.object(settings, "PROJECTROOT", str(root)),
+				patch.object(settings, "PROJECTS_LIST", str(root)),
+				patch.object(settings, "STRICT_EXPORT", False),
+				patch.object(settings, "EXPORT_OK", ""),
+				patch.object(settings, "AUTH", None),
+			):
+				client = _build_client()
+				yield {
+					"client": client,
+					"project": project,
+					"board": "Tasks",
+					"task_a_ref": task_a_ref,
+					"task_b_ref": task_b_ref,
+				}
+
+		def test_board_list_response_format(self, seeded_env: dict[str, str | TestClient]) -> None:
+			client = seeded_env["client"]
+			assert isinstance(client, TestClient)
+			resp = client.get("/board/list", params={"project": seeded_env["project"]})
+			assert resp.status_code == 200
+			body = resp.json()
+			assert isinstance(body, list)
+			assert body == [{"name": "Tasks", "description": "Main board", "task_count": 2}]
+
+		def test_task_list_response_format(self, seeded_env: dict[str, str | TestClient]) -> None:
+			client = seeded_env["client"]
+			assert isinstance(client, TestClient)
+			resp = client.get(
+				"/tasks/list",
+				params={"project": seeded_env["project"], "board": seeded_env["board"]},
+			)
+			assert resp.status_code == 200
+			body = resp.json()
+			assert isinstance(body, list)
+			tasks_by_ref = {entry["ref"]: entry for entry in body}
+			alpha = tasks_by_ref[str(seeded_env["task_a_ref"])]
+			assert alpha["title"] == "Alpha"
+			assert alpha["description"] == "Alpha description"
+			assert alpha["status"] == "TODO"
+			assert alpha["priority"] == "HIGH"
+			assert alpha["assignee"] == "alice"
+			assert alpha["comments_count"] == 1
+			assert alpha["created_at"] is not None
+			assert alpha["updated_at"] is not None
+
+		def test_comment_list_response_format(self, seeded_env: dict[str, str | TestClient]) -> None:
+			client = seeded_env["client"]
+			assert isinstance(client, TestClient)
+			resp = client.get(
+				"/comments/list",
+				params={
+					"project": seeded_env["project"],
+					"board": seeded_env["board"],
+					"task": seeded_env["task_a_ref"],
+				},
+			)
+			assert resp.status_code == 200
+			body = resp.json()
+			assert isinstance(body, list)
+			assert len(body) == 1
+			comment = body[0]
+			assert comment["content"] == "alpha note"
+			assert comment["tagger"] == "alice <alice@example.com>"
+			assert comment["created_at"] is not None
+			assert comment["edited_at"] is not None
+
+	class TestMutableResponses:
+		@pytest.fixture(scope="class")
+		def mutable_env(
+			self, tmp_path_factory: pytest.TempPathFactory
+		) -> Generator[dict[str, str | TestClient], None, None]:
+			root: Path = tmp_path_factory.mktemp("tasks-routes-mutable")
+			repo_dir: Path = root / "demo"
+			repo = pygit2.init_repository(str(repo_dir), bare=False)
+			_create_initial_commit(repo, repo_dir)
+			project: str = "demo"
+			board_name: str = "Tasks"
+			board_ref: str = f"{BOARD_REF_PREFIX}{board_name}"
+			board = Board(EMPTY_TREE_OID, board_ref, tagger="", description="Mutable board")
+			task_ref = f"{TASK_REF_PREFIX}mutable_seed"
+			task = Task(
+				EMPTY_TREE_OID,
+				task_ref,
+				tagger="",
+				title="Mutable seed",
+				description="",
+				status=Task.Status.TODO,
+				priority=Task.Priority.LOW,
+			)
+			task_oid = task.write(repo)
+			comment = Comment(task_oid, "seed <seed@example.com>", "seed comment")
+			comment_oid = comment.write(repo)
+			task.comments = [str(comment_oid)]
+			task.update_message()
+			task_oid = task.write(repo)
+			board.tasks = [str(task_oid)]
+			board.update_message()
+			board.write(repo)
+
+			with (
+				patch.object(settings, "PROJECTROOT", str(root)),
+				patch.object(settings, "PROJECTS_LIST", str(root)),
+				patch.object(settings, "STRICT_EXPORT", False),
+				patch.object(settings, "EXPORT_OK", ""),
+				patch.object(settings, "AUTH", None),
+			):
+				client = _build_client()
+				yield {
+					"client": client,
+					"project": project,
+					"board": board_name,
+					"seed_comment_oid": str(comment_oid),
+				}
+
+		def test_task_create_and_update_response_format(self, mutable_env: dict[str, str | TestClient]) -> None:
+			client = mutable_env["client"]
+			assert isinstance(client, TestClient)
+			create_resp = client.post(
+				"/tasks/create",
+				params={
+					"project": mutable_env["project"],
+					"board": mutable_env["board"],
+					"title": "Created Task",
+					"description": "Needs updates",
+					"status": "TODO",
+					"priority": "LOW",
+				},
+			)
+			assert create_resp.status_code == 200
+			created = create_resp.json()
+			assert set(created.keys()) == {"task_id", "ref", "oid"}
+			assert str(created["ref"]).startswith("refs/tags/tasks/task_")
+
+			update_resp = client.post(
+				"/tasks/update",
+				params={
+					"project": mutable_env["project"],
+					"board": mutable_env["board"],
+					"task": created["ref"],
+				},
+				json={
+					"title": "Updated Task",
+					"description": "Updated description",
+					"status": "IN_REVIEW",
+					"priority": "CRITICAL",
+					"assignee": "bob",
+					"due_date": "2025-02-03T04:05:06+00:00",
+				},
+			)
+			assert update_resp.status_code == 200
+			updated = update_resp.json()
+			assert updated["ref"] == created["ref"]
+			assert updated["title"] == "Updated Task"
+			assert updated["description"] == "Updated description"
+			assert updated["status"] == "IN_REVIEW"
+			assert updated["priority"] == "CRITICAL"
+			assert updated["assignee"] == "bob"
+			assert updated["due_date"] == "2025-02-03T04:05:06+00:00"
+
+		def test_comment_delete_response_format(self, mutable_env: dict[str, str | TestClient]) -> None:
+			client = mutable_env["client"]
+			assert isinstance(client, TestClient)
+			delete_comment_resp = client.post(
+				"/comments/delete",
+				params={
+					"project": mutable_env["project"],
+					"board": mutable_env["board"],
+					"comment": mutable_env["seed_comment_oid"],
+				},
+			)
+			assert delete_comment_resp.status_code == 200
+			assert delete_comment_resp.json() == {"message": "Comment deleted"}
+
+		def test_task_delete_response_format(self, mutable_env: dict[str, str | TestClient]) -> None:
+			client = mutable_env["client"]
+			assert isinstance(client, TestClient)
+			create_resp = client.post(
+				"/tasks/create",
+				params={
+					"project": mutable_env["project"],
+					"board": mutable_env["board"],
+					"title": "To delete",
+				},
+			)
+			task_ref: str = create_resp.json()["ref"]
+			delete_resp = client.post(
+				"/tasks/delete",
+				params={
+					"project": mutable_env["project"],
+					"board": mutable_env["board"],
+					"task": task_ref,
+				},
+			)
+			assert delete_resp.status_code == 200
+			assert delete_resp.json() == {"message": "Task deleted"}
+
+			repo = pygit2.Repository(str(Path(settings.PROJECTROOT) / str(mutable_env["project"])))
+			assert task_ref not in repo.references
+
+		def test_board_create_and_delete_response_format(self, mutable_env: dict[str, str | TestClient]) -> None:
+			client = mutable_env["client"]
+			assert isinstance(client, TestClient)
+			create_resp = client.post(
+				"/board/create",
+				params={
+					"project": mutable_env["project"],
+					"name": "temp-board",
+					"description": "Temporary",
+				},
+			)
+			assert create_resp.status_code == 200
+			created = create_resp.json()
+			assert set(created.keys()) == {"name", "ref", "oid"}
+			assert created["name"] == "temp-board"
+			assert created["ref"] == f"{BOARD_REF_PREFIX}temp-board"
+			Oid(hex=created["oid"])
+
+			delete_resp = client.post(
+				"/board/delete",
+				params={"project": mutable_env["project"], "name": "temp-board"},
+			)
+			assert delete_resp.status_code == 200
+			assert delete_resp.json() == {"message": "Board deleted"}
