# Implementation Plan: API Documentation Visibility Gate

**Branch**: `012-api-docs-gate` | **Date**: 2026-05-07 | **Spec**: [spec.md](spec.md)
**Input**: Feature specification from `specs/012-api-docs-gate/spec.md`

## Summary

Add `API_DOCS_ENABLED` (boolean, default `true`) to `app/config.py`. When `false`, pass `docs_url=None`, `redoc_url=None`, `openapi_url=None` to the `FastAPI()` constructor in `app/main.py`, making all three documentation routes return 404. A field validator provides graceful fallback for invalid flag values. Two new integration tests verify both flag states; the existing unit test suite is extended with two settings tests.

## Technical Context

**Language/Version**: Python 3.12
**Primary Dependencies**: FastAPI (constructor params), pydantic-settings (field validator)
**Storage**: None
**Testing**: pytest unit (`api/tests/unit/test_config.py`), pytest + ASGI test client (`api/tests/integration/test_docs_gate.py`)
**Target Platform**: API container (same as existing)
**Project Type**: Web service configuration change
**Performance Goals**: No measurable impact — one boolean read at startup
**Constraints**: Default must be `true` (backwards compatible); invalid env var value must not crash startup; no other routes affected
**Scale/Scope**: Three files changed (`config.py`, `main.py`, `.env.example`); one new test file; one existing test file extended

## Constitution Check

| Principle | Requirement | Status |
|-----------|-------------|--------|
| §5.1 TDD | Failing tests written before implementation | ✅ Tasks order tests first |
| §5.2 Integration tests | New integration tests follow existing pattern | ✅ |
| §5.3 Tests next to code | `api/tests/unit/` and `api/tests/integration/` | ✅ |
| §5.4 CI before done | All tests pass before task marked done | ✅ |
| §7.2 Env config | Flag via environment variable, not hardcoded | ✅ |
| §7.3 Linting | `ruff` passes on all changed files | ✅ Enforced in polish task |
| §2.6 No speculative abstraction | One boolean field, no plugin system | ✅ |

**No violations. All gates pass.**

## Project Structure

### Documentation (this feature)

```text
specs/012-api-docs-gate/
├── plan.md                    ← this file
├── research.md                ← 6 decisions
├── contracts/
│   └── docs-endpoints.md     ← behaviour contract for 3 affected endpoints
├── quickstart.md              ← 4 test scenarios
└── tasks.md                  ← generated by /speckit-tasks
```

### Source Code Changes

```text
api/
├── app/
│   ├── config.py              ← MODIFIED: add api_docs_enabled field + validator
│   └── main.py                ← MODIFIED: conditional docs_url/redoc_url/openapi_url
├── tests/
│   ├── unit/
│   │   └── test_config.py     ← MODIFIED: 2 new tests for api_docs_enabled
│   └── integration/
│       └── test_docs_gate.py  ← NEW: 2 integration tests (disabled + enabled)

.env.example                   ← MODIFIED: document API_DOCS_ENABLED
```

## Implementation Design

### `app/config.py` — new field with graceful fallback validator

```python
from pydantic import field_validator

class Settings(BaseSettings):
    # ... existing fields ...
    api_docs_enabled: bool = True

    @field_validator('api_docs_enabled', mode='before')
    @classmethod
    def coerce_docs_enabled(cls, v):
        if isinstance(v, bool):
            return v
        try:
            from pydantic import TypeAdapter
            return TypeAdapter(bool).validate_python(v)
        except Exception:
            return True  # FR-007: invalid value → safe default (enabled)
```

### `app/main.py` — conditional docs URLs

```python
_settings = get_settings()

app = FastAPI(
    title="Reactbin API",
    version="1.0.0",
    lifespan=lifespan,
    docs_url="/docs" if _settings.api_docs_enabled else None,
    redoc_url="/redoc" if _settings.api_docs_enabled else None,
    openapi_url="/openapi.json" if _settings.api_docs_enabled else None,
)
```

### Integration test pattern

The `app` object is constructed at module import time. Tests reload the module with the env var pre-set:

```python
def test_docs_disabled(monkeypatch, _base_env):
    monkeypatch.setenv("API_DOCS_ENABLED", "false")
    from app.config import get_settings
    get_settings.cache_clear()
    import importlib, app.main as m
    importlib.reload(m)
    client = TestClient(m.app)
    assert client.get("/docs").status_code == 404
    assert client.get("/redoc").status_code == 404
    assert client.get("/openapi.json").status_code == 404
    assert client.get("/api/v1/health").status_code == 200
```

`get_settings.cache_clear()` is required before the reload so the new env var is picked up.

### `.env.example` addition

```bash
# API documentation endpoints (Swagger UI, ReDoc, OpenAPI schema)
# Set to false in production to avoid exposing the API surface publicly.
API_DOCS_ENABLED=true
```

## Dependencies & Risks

| Item | Risk | Mitigation |
|------|------|------------|
| `@lru_cache` on `get_settings()` | Tests may pick up cached settings across reloads | Always call `get_settings.cache_clear()` before reloading `app.main` in tests |
| Module-level `get_settings()` in `main.py` | Import fails if required settings are absent (pre-existing behaviour) | Not a new risk; same as today |
| `openapi_url=None` | Disables HTTP route but not internal schema generation | Intentional; request validation is unaffected |
