--- name: api-endpoints description: End-to-end workflow for adding or changing a backend API endpoint — which module the serializer and view belong in, URL registration, OpenAPI client regeneration, and typed consumption from the frontend. Use when adding, changing, or removing a DRF endpoint or serializer. --- # Adding or changing an API endpoint The frontend consumes the backend exclusively through the generated typescript-fetch client at `frontend/src/openapi/`. Every API change runs the full loop below. ## 1. Serializer and view go in the surface's own module `overbae/api/` is one module per surface — `eval_views.py`/`eval_serializers.py`, `datasets.py`/`dataset_serializers.py`, `optimizer.py`, `billing.py`, `otlp.py`, and so on. `views.py` and `serializers.py` are the legacy monoliths; a new surface gets its own module rather than growing them. Never return a raw dict from a view — request and response shapes are serializers. Keep business logic in `services/` or on the model; annotate with `@extend_schema` where drf-spectacular needs help. Expected input failures may use `core.errors.InputValidationError` with an authored public `detail`. Never build that detail from a caught exception, provider response or stored diagnostic. Return the authored detail for known failures; log unexpected exceptions server-side and return a fixed message. Apply the same boundary to MCP adapters. ## 2. Register the route `overbae/urls.py`, on the `DefaultRouter` for viewsets: ```python router.register(r"widgets", WidgetViewSet, basename="widget") ``` or in `urlpatterns` for plain views. ## 3. Regenerate the client ```bash make generate_api_client ``` Settings refuse to load without Modal tokens when `.env` has none: prefix with `MODAL_TOKEN_ID=dummy MODAL_TOKEN_SECRET=dummy` — the schema step never talks to Modal. Runs `manage.py spectacular` → dockerized `openapi-generator-cli` → rewrites `frontend/src/openapi/{apis,models}`. The Compose frontend uses polling because Docker bind mounts can miss files recreated by generation. A method present on disk but missing at runtime can be a stale Vite transform: check the JavaScript served at `/src/openapi/apis/.ts`. Recreate the frontend with `docker compose up -d --no-deps frontend` after changing its watcher environment; restarting alone does not apply environment changes. ## 4. Register the API class in `client.ts` ```typescript import { WidgetsApi } from "./openapi"; export class API { widgets: WidgetsApi; constructor(private cfg: Configuration) { this.widgets = new WidgetsApi(this.cfg); } } ``` ## 5. Consume through the client, never raw fetch ```typescript import api from "@/client"; import type { Widget } from "@/openapi"; const widget: Widget = await api.widgets.widgetsRetrieve({ id }); ``` Hardcoded URLs and bare `fetch`/`axios` bypass auth middleware and typing. Response types come from `frontend/src/openapi/models/` — never redeclare a type that already exists there. ## Rules - Never hand-edit anything under `frontend/src/openapi/` — a hook blocks it under Claude Code, and the next regeneration would overwrite it anyway. - A DRF view docstring becomes the OpenAPI operation `description`, so editing one changes the generated client. Prefer an explicit `@extend_schema(summary=...)`.