# API Contract

Base URL (development): `http://localhost:9999`  
The Vite dev server proxies `/api` to the backend.

All responses are JSON. Timestamps are ISO 8601 strings (SQLite `datetime('now')`).

---

## GET /health

Health check.

**Query**

| Param    | Description                          |
|----------|--------------------------------------|
| `format` | Set to `json` for JSON response      |

**Response (JSON)** — `?format=json`

```json
{
  "status": "ok",
  "service": "resource-catalog"
}
```

---

## GET /api/items

List published catalog items with optional filters.

**Query parameters**

| Param      | Type   | Description                                      |
|------------|--------|--------------------------------------------------|
| `search`   | string | Match `title` or `description` (case-insensitive substring) |
| `category` | string | Category slug                                    |
| `type`     | string | Resource type slug (e.g. `ai-model`, `api`)      |
| `pricing`  | string | Pricing type: `free`, `freemium`, `paid`, `open-source` |
| `tag`      | string | Tag slug                                         |

**Response** `200`

```json
{
  "items": [
    {
      "id": 1,
      "title": "Example",
      "slug": "example",
      "type": "api",
      "description": "Short summary",
      "long_description": "Longer text or null",
      "website_url": "https://example.com",
      "image_url": "/api/images/items/example/0.svg",
      "pricing_type": "free",
      "status": "published",
      "created_at": "2026-05-15 12:00:00",
      "updated_at": "2026-05-15 12:00:00",
      "images": [
        {
          "id": 1,
          "url": "/api/images/items/example/0.svg",
          "alt_text": "Example — photo 1",
          "sort_order": 0
        }
      ],
      "categories": [{ "id": 1, "name": "Development", "slug": "development" }],
      "tags": [{ "id": 1, "name": "REST", "slug": "rest" }]
    }
  ],
  "total": 1
}
```

---

## GET /api/items/:slug

Single item by slug.

**Response** `200`

```json
{
  "item": { }
}
```

Same shape as one element in `items` above (includes `categories` and `tags`).

**Response** `404`

```json
{
  "error": "Item not found"
}
```

---

## GET /api/categories

All categories with published item counts.

**Response** `200`

```json
{
  "categories": [
    {
      "id": 1,
      "name": "Development",
      "slug": "development",
      "description": "Libraries, APIs, and dev tools",
      "item_count": 8
    }
  ]
}
```

---

## GET /api/tags

All tags with published item counts.

**Response** `200`

```json
{
  "tags": [
    {
      "id": 1,
      "name": "Python",
      "slug": "python",
      "item_count": 2
    }
  ]
}
```

---

## Item fields

| Field              | Type    | Notes |
|--------------------|---------|-------|
| `id`               | integer | Primary key |
| `title`            | string  | Display name |
| `slug`             | string  | URL-safe unique identifier |
| `type`             | string  | See resource types below |
| `description`      | string  | Short summary |
| `long_description` | string? | Full description |
| `website_url`      | string? | External link |
| `image_url`        | string? | Cover image (first gallery photo when a gallery exists) |
| `images`           | array   | Gallery photos; see `item_images` below |
| `pricing_type`     | string  | `free`, `freemium`, `paid`, `open-source` |
| `status`           | string  | `published` (only published items are returned) |
| `created_at`       | string  | |
| `updated_at`       | string  | |

### Gallery image (`images[]`)

| Field        | Type    | Notes |
|--------------|---------|-------|
| `id`         | integer | Primary key |
| `url`        | string  | Path `/api/images/items/{slug}/{file}` or absolute URL when `PUBLIC_API_URL` is set |
| `alt_text`   | string? | Accessibility label |
| `sort_order` | integer | Display order (ascending) |

Stored in the `item_images` table. Files live on disk under `server/data/images/`. Each seeded item includes up to four photos.

### GET /api/items/:slug/images

List gallery images for a **published** item (no auth).

**Response** `200`

```json
{
  "item_id": 1,
  "slug": "example",
  "images": [
    {
      "id": 1,
      "url": "/api/images/items/example/0.jpg",
      "alt_text": "Example — 1",
      "sort_order": 0
    }
  ]
}
```

**Response** `404` — item not found or not published.

### GET /api/items/:slug/images/:imageId

Single gallery image for a published item.

**Response** `200`

```json
{
  "item_id": 1,
  "slug": "example",
  "image": {
    "id": 1,
    "url": "/api/images/items/example/0.jpg",
    "alt_text": "Example — 1",
    "sort_order": 0
  }
}
```

When `PUBLIC_API_URL` is set, `url` values are absolute.

### GET /api/images/items/{slug}/{filename}

Serves static image files (no auth). Example: `/api/images/items/react/0.svg`.

When `PUBLIC_API_URL` is configured in `.env`, list/detail responses rewrite relative paths to `{PUBLIC_API_URL}/api/images/...` for mobile clients.

### Resource types (`type`)

- `ai-model`
- `software-tool`
- `website`
- `course`
- `api`
- `library`
- `open-source`
- `service`
- `tutorial`
- `template`
- `prompt-collection`

---

## Errors

Unhandled server errors return `500` with a plain text or default Express body. Client should treat non-2xx responses as failures.

---

## Admin API

All admin routes require header `Authorization: Bearer <ADMIN_API_KEY>` (or `X-Admin-Key`). Returns `401` if missing/invalid, `503` if `ADMIN_API_KEY` is not set on the server.

### POST /api/admin/verify

Validates the key. Response `200`: `{ "ok": true }`

### GET /api/admin/meta

Categories and tags for forms (id, name, slug only).

### GET /api/admin/items

All items (any status), with relations. Response: `{ items, total }`

### GET /api/admin/items/:id

Single item by numeric id.

### POST /api/admin/items

Create item. Body:

```json
{
  "title": "Example",
  "slug": "example",
  "type": "api",
  "description": "Short text",
  "long_description": "Optional",
  "website_url": "https://example.com",
  "pricing_type": "free",
  "status": "published",
  "category_ids": [1, 2],
  "tag_ids": [3]
}
```

Gallery images are managed via the **Item images** admin endpoints below, not in the item body.

Response `201`: `{ item }`. Errors `400` with `{ error }`.

### PUT /api/admin/items/:id

Same body as POST. Response `200`: `{ item }`.

### DELETE /api/admin/items/:id

Response `200`: `{ "ok": true }`

### Item images (admin)

Base path: `/api/admin/items/:itemId/images`  
Requires admin auth. Upload uses `multipart/form-data` with field `file` (max 10 MB, image types only). Optional fields: `alt_text`, `sort_order`.

#### GET /api/admin/items/:itemId/images

Response `200`: `{ item_id, slug, images: [...] }`

#### GET /api/admin/items/:itemId/images/:imageId

Response `200`: `{ image }`

#### POST /api/admin/items/:itemId/images

Upload a file. Response `201`: `{ image }`

#### PUT /api/admin/items/:itemId/images/:imageId

Update metadata. Body JSON: `{ "alt_text": "...", "sort_order": 0 }`. Response `200`: `{ image }`

#### DELETE /api/admin/items/:itemId/images/:imageId

Removes DB row and file on disk. Response `200`: `{ "ok": true }`

### Item status values

- `published` — visible on public API
- `draft` — admin only
- `archived` — admin only

---

## Future extensions

- User accounts and session-based auth
- Ratings and comments
- Pagination (`page`, `limit`)
