# 05 | Owned project APIs

## Goal

Create, list, read, update and delete projects belonging to the signed-in customer.

## What you are building

owned_project combines the requested ID with the authenticated owner's ID in the ORM query. Every detail, edit and delete uses that guard. Creation takes owner_id from the session rather than accepting it from JSON. list_projects filters before returning rows. A nonexistent project and another person's project both return 404. The small HTTP demo establishes cookies and CSRF automatically, registers an account and exercises a working project endpoint.

## Build this chapter

Stop the server. Work in project_manager. Follow each CREATE, whole-file replacement, or marked-block edit below in order.

## Step 1: CREATE - make parent folders, then create this file - modules/projects/__init__.py

This file owns the feature named by its module or component. Read the route/form flow before continuing.

Create the parent directories, then save this complete file.

```python
"""Module-owned APIs and interface files."""
```

**Say:** "This file owns the feature named by its module or component. Read the route/form flow before continuing."

**Type:** Follow the action above in `modules/projects/__init__.py`; explain each new function or binding as you add it.

**Viewers should see:** The saved file matches this step. After all steps, run the chapter checks below and show the expected result.

## Step 2: CREATE - make parent folders, then create this file - modules/projects/module.py

Create the projects module, then implement owned_project before the handlers that use it. The mount adds /api/projects once. Every ORM query for an existing project includes the signed-in customer's owner ID.

Create the parent directories, then save this complete file.

```python
"""Project APIs always constrain queries by the signed-in user's id."""

from pathlib import Path
from models import Project
from flaxon.modules import FlaxonModule
from flaxon.http import JSONResponse
from flaxon.exceptions import NotFound
from security import require_user, check_csrf
from validation import json_object, text

projects = FlaxonModule(
    "projects",
    ui_dir=Path(__file__).parent / "ui",
    ui_routes={
        "ProjectList.js": "/projects",
        "ProjectDetails/[id].js": "/projects/:id",
    },
)


async def owned_project(request, project_id):
    user = await require_user(request)
    project = await Project.filter(id=project_id, owner_id=user["id"]).first().values()
    if project is None:
        raise NotFound("Project not found.")
    return project


@projects.get("/")
async def list_projects(request):
    user = await require_user(request)
    items = await Project.filter(owner_id=user["id"]).order_by("-id").values()
    return {"data": items}


@projects.post("/")
async def create_project(request):
    await check_csrf(request)
    user = await require_user(request)
    data = await json_object(request)
    name = text(data, "name")
    description = text(data, "description", 1000, required=False)
    project = await Project.create(
        owner_id=user["id"], name=name, description=description
    )
    project_id = project.id
    return JSONResponse(
        {"data": await owned_project(request, project_id)}, status_code=201
    )


@projects.get("/<int:project_id>")
async def get_project(request, project_id):
    return {"data": await owned_project(request, project_id)}


@projects.put("/<int:project_id>")
async def update_project(request, project_id):
    await check_csrf(request)
    project = await owned_project(request, project_id)
    data = await json_object(request)
    name = text(data, "name")
    description = text(data, "description", 1000, required=False)
    await Project.filter(id=project_id, owner_id=project["owner_id"]).update(
        name=name, description=description
    )
    return {"data": await owned_project(request, project_id)}


@projects.delete("/<int:project_id>")
async def delete_project(request, project_id):
    await check_csrf(request)
    project = await owned_project(request, project_id)
    await Project.filter(id=project_id, owner_id=project["owner_id"]).delete()
    return {"data": {"deleted": True}}
```

**Say:** "Create the projects module, then implement owned_project before the handlers that use it. The mount adds /api/projects once. Every ORM query for an existing project includes the signed-in customer's owner ID."

**Type:** Follow the action above in `modules/projects/module.py`; explain each new function or binding as you add it.

**Viewers should see:** The saved file matches this step. After all steps, run the chapter checks below and show the expected result.

## Step 3: CREATE - make parent folders, then create this file - scripts/course_api_demo.py

This file owns the feature named by its module or component. Read the route/form flow before continuing.

Create the parent directories, then save this complete file.

```python
"""Show the project API over HTTP; start the development server first."""
import secrets
import httpx


def main():
    # Each run creates its own learner and project in your development database.
    with httpx.Client(base_url="http://127.0.0.1:8000", trust_env=False) as client:
        response = client.get("/api/auth/session")
        response.raise_for_status()
        csrf = response.json()["data"]["csrf"]
        response = client.post(
            "/api/auth/register",
            headers={"X-CSRF-Token": csrf},
            json={"name": "Sample Learner", "email": f"sample-{secrets.token_hex(6)}@example.test", "password": "LearningPython123!"},
        )
        response.raise_for_status()
        csrf = response.json()["data"]["csrf"]
        response = client.post(
            "/api/projects/", headers={"X-CSRF-Token": csrf},
            json={"name": "My first project", "description": "Record the sample lesson"},
        )
        response.raise_for_status()
        assert response.status_code == 201
        project = response.json()["data"]
        print("Created:", project)
        response = client.get(f"/api/projects/{project['id']}")
        response.raise_for_status()
        print("Read:", response.json()["data"])
        response = client.post("/api/projects/", headers={"X-CSRF-Token": csrf}, json={"name": " "})
        assert response.status_code == 400
        print("Invalid input rejected:", response.status_code)
        print("Use your own browser account to create a separate project on /projects.")


if __name__ == "__main__":
    main()
```

**Say:** "This file owns the feature named by its module or component. Read the route/form flow before continuing."

**Type:** Follow the action above in `scripts/course_api_demo.py`; explain each new function or binding as you add it.

**Viewers should see:** The saved file matches this step. After all steps, run the chapter checks below and show the expected result.

## Step 4: EDIT - replace the marked blocks - app.py

The application factory mounts only the features already introduced. Follow the action label: a marked-block edit keeps the surrounding factory intact.

### Edit 1: find this exact block

Open `app.py`. Locate this block once. The unchanged surrounding lines identify its position; do not use line numbers from another revision.

```python
from flaxon import Flaxon, Request
from flaxon.http import JSONResponse
from modules.auth.module import auth
from modules.welcome.module import welcome
from settings import (
    ROOT,
```
### Replace that block with

```python
from flaxon import Flaxon, Request
from flaxon.http import JSONResponse
from modules.auth.module import auth
from modules.projects.module import projects
from modules.welcome.module import welcome
from settings import (
    ROOT,
```

Leave the rest of the file unchanged.

### Edit 2: find this exact block

Open `app.py`. Locate this block once. The unchanged surrounding lines identify its position; do not use line numbers from another revision.

```python
    app.dummy_password_hash = app.hasher.hash("dummy-password-for-timing-only")
    for module, prefix in [
        (auth, "/api/auth"),
        (welcome, "/api/welcome"),
    ]:
        app.mount_module(module, prefix=prefix)
```
### Replace that block with

```python
    app.dummy_password_hash = app.hasher.hash("dummy-password-for-timing-only")
    for module, prefix in [
        (auth, "/api/auth"),
        (projects, "/api/projects"),
        (welcome, "/api/welcome"),
    ]:
        app.mount_module(module, prefix=prefix)
```

Leave the rest of the file unchanged.

**Say:** "The application factory mounts only the features already introduced. Follow the action label: a marked-block edit keeps the surrounding factory intact."

**Type:** Follow the action above in `app.py`; explain each new function or binding as you add it.

**Viewers should see:** The saved file matches this step. After all steps, run the chapter checks below and show the expected result.

## Run and check

```bash
python management.py runserver
# In a second terminal:
python scripts/course_api_demo.py
```

## Expected result

The HTTP demo creates a project and reads it back. Projects return a data envelope with the owner, name and description. A second customer's request cannot retrieve it.

## Common errors

Do not repeat /api/projects inside decorators: app.py supplies that mount prefix. Use a trailing slash for the list/create endpoint. A 401 means no valid customer session; 404 after sign-in can be an ownership denial.

## Short exercise

Extend the demo to update a project's description and assert the read response changed.

## What to say and show

Say: "Create, list, read, update and delete projects belonging to the signed-in customer. The server remains responsible for persistence and authorization; the browser presents the result."

Show the expected result above, then point to the file responsible for it. Run the chapter check before the next lesson.

## Save your checkpoint

```bash
git add .
git commit -m "Complete chapter 05: Owned project APIs"
```
