# Notes API — Benchmark Specification v1.0

Build a REST API for managing notes with tags. No authentication. No frontend. Seven endpoints plus a seed endpoint. Use whatever database your platform provides (Postgres preferred, SQLite acceptable).

All responses: `Content-Type: application/json`. All timestamps: ISO 8601. CORS enabled (`Access-Control-Allow-Origin: *`).

---

## Database Schema

Two tables:

```sql
CREATE TABLE notes (
  id         SERIAL PRIMARY KEY,
  title      VARCHAR(255) NOT NULL,
  content    TEXT NOT NULL DEFAULT '',
  category   VARCHAR(100) NOT NULL DEFAULT 'general',
  priority   INTEGER NOT NULL DEFAULT 0,
  is_pinned  BOOLEAN NOT NULL DEFAULT false,
  word_count INTEGER NOT NULL DEFAULT 0,
  created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
  updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);

CREATE TABLE tags (
  id      SERIAL PRIMARY KEY,
  note_id INTEGER NOT NULL REFERENCES notes(id) ON DELETE CASCADE,
  name    VARCHAR(100) NOT NULL
);

CREATE INDEX idx_notes_category ON notes(category);
CREATE INDEX idx_notes_priority ON notes(priority);
CREATE INDEX idx_notes_created  ON notes(created_at);
CREATE INDEX idx_tags_note_id   ON tags(note_id);
CREATE INDEX idx_tags_name      ON tags(name);
```

SQLite adaptation: use `INTEGER PRIMARY KEY AUTOINCREMENT` instead of `SERIAL`. Use `DATETIME` instead of `TIMESTAMP WITH TIME ZONE`. Skip the `ON DELETE CASCADE` if not supported and handle cascade deletes in application code.

---

## Endpoints

### 1. `GET /api/ping`

Health check. No database work.

**Response:**
```json
{ "status": "ok", "timestamp": "2026-04-30T14:23:00.000Z" }
```

---

### 2. `POST /api/notes`

Create a note.

**Request body:**
```json
{
  "title": "Meeting notes",
  "content": "Discussed the Q3 roadmap and assigned owners to each initiative.",
  "category": "meeting-notes",
  "priority": 3,
  "is_pinned": false,
  "tags": ["work", "q3", "roadmap"]
}
```

- `title` (required, string, max 255 chars)
- `content` (optional, string, default `""`)
- `category` (optional, string, default `"general"`)
- `priority` (optional, integer 0-5, default `0`)
- `is_pinned` (optional, boolean, default `false`)
- `tags` (optional, array of strings, default `[]`)

Compute `word_count` server-side by splitting `content` on whitespace.

**Response (201):**
```json
{
  "note": {
    "id": 1,
    "title": "Meeting notes",
    "content": "Discussed the Q3 roadmap and assigned owners to each initiative.",
    "category": "meeting-notes",
    "priority": 3,
    "is_pinned": false,
    "word_count": 10,
    "created_at": "2026-04-30T14:23:00.000Z",
    "updated_at": "2026-04-30T14:23:00.000Z",
    "tags": ["work", "q3", "roadmap"]
  }
}
```

Insert into `notes` table, then insert each tag into `tags` table with the note's `id`.

---

### 3. `GET /api/notes/:id`

Read a single note with its tags.

**Response (200):**
```json
{
  "note": {
    "id": 1,
    "title": "Meeting notes",
    "content": "...",
    "category": "meeting-notes",
    "priority": 3,
    "is_pinned": false,
    "word_count": 10,
    "created_at": "2026-04-30T14:23:00.000Z",
    "updated_at": "2026-04-30T14:23:00.000Z",
    "tags": ["work", "q3", "roadmap"]
  }
}
```

Must query both `notes` and `tags` tables (JOIN or two queries).

**Error (404):** `{ "error": "Note not found" }`

---

### 4. `PUT /api/notes/:id`

Update a note. Full replacement of provided fields.

**Request body:** Same shape as POST. All fields optional except `title`.

**Behavior:**
1. Update the `notes` row (set `updated_at` to now, recompute `word_count` if `content` changed).
2. Delete all existing tags for this note from the `tags` table.
3. Insert new tags from the request body.

This tests write + delete + insert in a single request.

**Response (200):** Same shape as GET — returns the updated note with new tags.

**Error (404):** `{ "error": "Note not found" }`

---

### 5. `DELETE /api/notes/:id`

Delete a note and all its tags.

**Response (200):**
```json
{ "deleted": true }
```

**Error (404):** `{ "error": "Note not found" }`

---

### 6. `GET /api/notes`

List notes with optional filtering and pagination.

**Query parameters:**
- `category` (optional) — filter by exact category match
- `priority` (optional) — filter by exact priority value
- `page` (optional, default `1`) — 1-indexed page number
- `limit` (optional, default `20`, max `100`) — items per page

**Response (200):**
```json
{
  "notes": [
    {
      "id": 1,
      "title": "Meeting notes",
      "content": "...",
      "category": "meeting-notes",
      "priority": 3,
      "is_pinned": false,
      "word_count": 10,
      "created_at": "2026-04-30T14:23:00.000Z",
      "updated_at": "2026-04-30T14:23:00.000Z",
      "tags": ["work", "q3", "roadmap"]
    }
  ],
  "total": 847,
  "page": 1,
  "limit": 20
}
```

Each note must include its tags. Sort by `created_at DESC`. The `total` field is the count of all matching notes (before pagination), not just the returned page.

---

### 7. `GET /api/notes/stats`

Aggregation endpoint. Returns summary statistics across all notes.

**Response (200):**
```json
{
  "total": 10000,
  "byCategory": {
    "general": 1012,
    "work": 1023,
    "personal": 998,
    "ideas": 1001,
    "meeting-notes": 987,
    "research": 1010,
    "todo": 1005,
    "journal": 992,
    "reference": 978,
    "archive": 994
  },
  "avgPriority": 2.48,
  "totalTags": 24832,
  "pinnedCount": 312
}
```

Requires:
- `total` — `SELECT COUNT(*) FROM notes`
- `byCategory` — `SELECT category, COUNT(*) FROM notes GROUP BY category`
- `avgPriority` — `SELECT AVG(priority) FROM notes` (round to 2 decimals)
- `totalTags` — `SELECT COUNT(*) FROM tags`
- `pinnedCount` — `SELECT COUNT(*) FROM notes WHERE is_pinned = true`

---

### 8. `POST /api/seed`

Populate the database with test data. Called once before benchmarks. **This endpoint is not benchmarked.**

**Request body (optional):**
```json
{ "count": 10000 }
```

Default count: `10000`.

**Behavior:**

1. Delete all existing notes and tags (truncate or delete).
2. Generate `count` notes using this algorithm:

```
CATEGORIES = ["general", "work", "personal", "ideas", "meeting-notes",
              "research", "todo", "journal", "reference", "archive"]

TAG_POOL = ["urgent", "review", "draft", "final", "shared", "private",
            "follow-up", "blocked", "in-progress", "done", "bug", "feature",
            "documentation", "design", "backend", "frontend", "devops",
            "security", "performance", "ux", "api", "database", "testing",
            "deployment", "monitoring", "logging", "auth", "payments",
            "notifications", "search", "analytics", "reporting", "integration",
            "migration", "refactor", "cleanup", "optimization", "scaling",
            "caching", "queue", "scheduler", "webhook", "email", "sms",
            "push-notification", "file-upload", "export", "import", "backup",
            "restore", "audit", "compliance"]

For each note i (0 to count-1):
  title    = "Note " + i
  content  = "Benchmark content for note " + i + ". " repeated (1 + i % 20) times
  category = CATEGORIES[i % 10]
  priority = i % 6
  is_pinned = (i % 33 == 0)
  word_count = count words in content
  tags     = TAG_POOL[i % 50], TAG_POOL[(i * 7) % 50], TAG_POOL[(i * 13) % 50]
             (take first (i % 4) tags — 0 to 3 tags per note)
  created_at = (some timestamp, any reasonable value)
```

3. Insert all notes and tags in batch for performance.

**Response (200):**
```json
{ "seeded": 10000, "tags": 24832 }
```

---

## What NOT to build

- No authentication / authorization
- No frontend / HTML pages
- No file uploads
- No WebSocket / real-time
- No email sending
- No rate limiting

Just the eight endpoints above. The goal is a minimal, correct REST API that exercises database reads, writes, joins, and aggregations.

---

## Error handling

- `400` for invalid request body (missing required fields, invalid types)
- `404` for note not found on GET/PUT/DELETE by id
- `500` for unexpected server errors

Error response shape: `{ "error": "description" }`

---

## Performance requirements

- The API must handle concurrent requests. No global locks.
- Database writes should use transactions where appropriate (e.g., creating a note and its tags).
- The seed endpoint may take several seconds — that's fine. Use batch inserts.
