docs: deduplicate README, add OpenAPI spec with auto-generated API docs

- Slim README to essentials, link to docs/ for details
- Add docs/openapi.yaml as single source of truth for the API
- Add scripts/generate-api-docs.ts to produce docs/API.md from the spec
- Add pnpm docs:api / make docs-api target

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Frederic Cabassut
2026-03-05 21:24:29 +01:00
co-authored by Claude Opus 4.6
parent 66c91df983
commit 26cae27857
8 changed files with 1103 additions and 249 deletions
+397 -87
View File
@@ -1,166 +1,476 @@
# API Reference
<!-- AUTO-GENERATED from docs/openapi.yaml — do not edit manually -->
<!-- Run: pnpm docs:api -->
All API endpoints require authentication (session cookie) except `/api/auth/setup` and `/api/auth/login`.
# Crumbs API
## Authentication
REST API for Crumbs by Bretzel — a self-hostable, offline-first notes app.
### POST /api/auth/setup
First-time setup. Creates the single user account.
Base URL: `http://localhost:3000`
All endpoints require a `session` cookie (set by login) except where noted.
## Auth
### `POST /api/auth/setup`
First-time password setup
Creates the single user account. Only works once — returns 400 if already set up.
> No authentication required.
**Request:**
```json
{ "password": "min8characters" }
{
"password": "password"
}
```
**Response:** `201 Created`
**Response:** `201`
```json
{ "success": true }
{
"success": true
}
```
Sets `session` cookie.
**Errors:** `400` Setup already completed or invalid password
---
### `POST /api/auth/login`
Log in
> No authentication required.
### POST /api/auth/login
**Request:**
```json
{ "password": "yourpassword" }
{
"password": "password"
}
```
**Response:** `200 OK`
```json
{ "success": true }
```
Sets `session` cookie.
**Response:** `200`
### POST /api/auth/logout
Clears session.
**Response:** `200 OK`
```json
{ "success": true }
{
"success": true
}
```
**Errors:** `401` Invalid password
---
### `POST /api/auth/logout`
Log out
**Response:** `200`
```json
{
"success": true
}
```
---
## Notes
### GET /api/notes
List notes by filter.
### `GET /api/notes`
**Query Parameters:**
- `filter` - `all` (default), `archived`, `trashed`
List notes
**Parameters:**
| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `filter` | query | string | no | (all, archived, trashed) — default: `all` |
**Response:** `200`
**Response:** `200 OK`
```json
[
{
"id": "uuid",
"title": "My Note",
"content": "**Markdown** content",
"color": "coral",
"title": "title",
"content": "content",
"color": "default",
"pinned": true,
"archived": false,
"trashed": false,
"checklistMode": false,
"sortOrder": 0,
"createdAt": "2024-01-01T00:00:00.000Z",
"updatedAt": "2024-01-01T00:00:00.000Z",
"archived": true,
"trashed": true,
"checklistMode": true,
"sortOrder": 1,
"createdAt": "2025-01-01T00:00:00.000Z",
"updatedAt": "2025-01-01T00:00:00.000Z",
"version": 1,
"tags": ["work", "important"]
"tags": [
"item"
]
}
]
```
### POST /api/notes
Create a new note.
---
### `POST /api/notes`
Create a note
**Request:**
```json
{
"title": "My Note",
"content": "Note content with #tags",
"title": "title",
"content": "content",
"color": "default",
"pinned": false,
"checklistMode": false
"pinned": true,
"archived": true,
"trashed": true,
"checklistMode": true,
"sortOrder": 1
}
```
**Response:** `201 Created` - Returns the created note.
**Response:** `201`
### GET /api/notes/:id
Get a single note.
### PATCH /api/notes/:id
Update a note.
**Request:** (partial update - only include changed fields)
```json
{
"title": "Updated Title",
"color": "coral",
"pinned": true
"id": "uuid",
"title": "title",
"content": "content",
"color": "default",
"pinned": true,
"archived": true,
"trashed": true,
"checklistMode": true,
"sortOrder": 1,
"createdAt": "2025-01-01T00:00:00.000Z",
"updatedAt": "2025-01-01T00:00:00.000Z",
"version": 1,
"tags": [
"item"
]
}
```
**Response:** `200 OK` - Returns the updated note.
---
### DELETE /api/notes/:id
Permanently delete a note.
### `GET /api/notes/{id}`
## Search
Get a note
### GET /api/search
Full-text search across notes.
**Parameters:**
**Query Parameters:**
- `q` - Search query
| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `id` | path | string | yes | |
**Response:** `200 OK` - Array of matching notes (excluding trashed).
**Response:** `200`
## Tags
### GET /api/tags
List all tags.
**Response:** `200 OK`
```json
[
{ "id": 1, "name": "work" },
{ "id": 2, "name": "personal" }
]
{
"id": "uuid",
"title": "title",
"content": "content",
"color": "default",
"pinned": true,
"archived": true,
"trashed": true,
"checklistMode": true,
"sortOrder": 1,
"createdAt": "2025-01-01T00:00:00.000Z",
"updatedAt": "2025-01-01T00:00:00.000Z",
"version": 1,
"tags": [
"item"
]
}
```
**Errors:** `404` Note not found
---
### `PATCH /api/notes/{id}`
Update a note
Partial update — only include changed fields.
**Parameters:**
| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `id` | path | string | yes | |
**Request:**
```json
{
"title": "title",
"content": "content",
"color": "default",
"pinned": true,
"archived": true,
"trashed": true,
"checklistMode": true,
"sortOrder": 1
}
```
**Response:** `200`
```json
{
"id": "uuid",
"title": "title",
"content": "content",
"color": "default",
"pinned": true,
"archived": true,
"trashed": true,
"checklistMode": true,
"sortOrder": 1,
"createdAt": "2025-01-01T00:00:00.000Z",
"updatedAt": "2025-01-01T00:00:00.000Z",
"version": 1,
"tags": [
"item"
]
}
```
**Errors:** `404` Note not found
---
### `DELETE /api/notes/{id}`
Permanently delete a note
**Parameters:**
| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `id` | path | string | yes | |
**Response:** `200`
```json
{
"success": true
}
```
**Errors:** `404` Note not found
---
## Attachments
### GET /api/notes/:id/attachments
List attachments for a note.
### `GET /api/notes/{id}/attachments`
### POST /api/notes/:id/attachments
Upload an image attachment.
List or download attachments
Without `attachmentId`, lists all attachments. With `attachmentId`, returns the file.
**Parameters:**
| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `id` | path | string | yes | |
| `attachmentId` | query | string | no | |
**Response:** `200`
---
### `POST /api/notes/{id}/attachments`
Upload an image attachment
**Parameters:**
| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `id` | path | string | yes | |
**Request:** `multipart/form-data` with `file` field.
**Response:** `201 Created` - Returns attachment metadata.
**Response:** `201`
### DELETE /api/notes/:id/attachments?attachmentId=xxx
Delete an attachment.
```json
{
"id": "id",
"noteId": "noteId",
"filename": "filename",
"mimeType": "mimeType",
"size": 1,
"createdAt": "2025-01-01T00:00:00.000Z"
}
```
**Errors:** `400` Invalid file
---
### `DELETE /api/notes/{id}/attachments`
Delete an attachment
**Parameters:**
| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `id` | path | string | yes | |
| `attachmentId` | query | string | yes | |
**Response:** `200`
```json
{
"success": true
}
```
---
## Search
### `GET /api/search`
Full-text search
Searches across note titles, content, and tags. Excludes trashed notes.
**Parameters:**
| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `q` | query | string | yes | |
**Response:** `200`
```json
[
{
"id": "uuid",
"title": "title",
"content": "content",
"color": "default",
"pinned": true,
"archived": true,
"trashed": true,
"checklistMode": true,
"sortOrder": 1,
"createdAt": "2025-01-01T00:00:00.000Z",
"updatedAt": "2025-01-01T00:00:00.000Z",
"version": 1,
"tags": [
"item"
]
}
]
```
---
## Tags
### `GET /api/tags`
List all tags
**Response:** `200`
```json
[
{
"id": 1,
"name": "name"
}
]
```
---
## Sync
### POST /api/sync
Push local changes to server.
### `GET /api/sync`
Pull changes since timestamp
**Parameters:**
| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `since` | query | integer | yes | Unix timestamp in milliseconds |
**Response:** `200`
```json
[
{
"id": "uuid",
"title": "title",
"content": "content",
"color": "default",
"pinned": true,
"archived": true,
"trashed": true,
"checklistMode": true,
"sortOrder": 1,
"createdAt": "2025-01-01T00:00:00.000Z",
"updatedAt": "2025-01-01T00:00:00.000Z",
"version": 1,
"tags": [
"item"
]
}
]
```
---
### `POST /api/sync`
Push local changes
**Request:**
```json
{
"changes": [
{
"noteId": "uuid",
"operation": "create|update|delete",
"timestamp": 1704067200000,
"data": { "title": "...", "content": "..." }
"operation": "create",
"timestamp": 1,
"data": {
"title": "title",
"content": "content",
"color": "default",
"pinned": true,
"archived": true,
"trashed": true,
"checklistMode": true,
"sortOrder": 1
}
}
]
}
```
### GET /api/sync?since=timestamp
Pull changes since a timestamp (milliseconds).
**Response:** `200`
**Response:** `200 OK` - Array of notes updated since the given timestamp.
```json
{
"success": true
}
```
---
+420
View File
@@ -0,0 +1,420 @@
openapi: 3.1.0
info:
title: Crumbs API
description: REST API for Crumbs by Bretzel — a self-hostable, offline-first notes app.
version: 0.1.0
contact:
name: Bretzel
url: https://bretzel.app
servers:
- url: http://localhost:3000
description: Local development
security:
- sessionCookie: []
paths:
/api/auth/setup:
post:
summary: First-time password setup
description: Creates the single user account. Only works once — returns 400 if already set up.
tags: [Auth]
security: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [password]
properties:
password:
type: string
minLength: 8
responses:
'201':
description: Account created, session cookie set
content:
application/json:
schema:
$ref: '#/components/schemas/Success'
'400':
description: Setup already completed or invalid password
/api/auth/login:
post:
summary: Log in
tags: [Auth]
security: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [password]
properties:
password:
type: string
responses:
'200':
description: Logged in, session cookie set
content:
application/json:
schema:
$ref: '#/components/schemas/Success'
'401':
description: Invalid password
/api/auth/logout:
post:
summary: Log out
tags: [Auth]
responses:
'200':
description: Session cleared
content:
application/json:
schema:
$ref: '#/components/schemas/Success'
/api/notes:
get:
summary: List notes
tags: [Notes]
parameters:
- name: filter
in: query
schema:
type: string
enum: [all, archived, trashed]
default: all
responses:
'200':
description: Array of notes
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Note'
post:
summary: Create a note
tags: [Notes]
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/NoteInput'
responses:
'201':
description: Created note
content:
application/json:
schema:
$ref: '#/components/schemas/Note'
/api/notes/{id}:
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
get:
summary: Get a note
tags: [Notes]
responses:
'200':
description: Note with tags
content:
application/json:
schema:
$ref: '#/components/schemas/Note'
'404':
description: Note not found
patch:
summary: Update a note
description: Partial update — only include changed fields.
tags: [Notes]
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/NoteInput'
responses:
'200':
description: Updated note
content:
application/json:
schema:
$ref: '#/components/schemas/Note'
'404':
description: Note not found
delete:
summary: Permanently delete a note
tags: [Notes]
responses:
'200':
description: Deleted
content:
application/json:
schema:
$ref: '#/components/schemas/Success'
'404':
description: Note not found
/api/notes/{id}/attachments:
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
get:
summary: List or download attachments
description: Without `attachmentId`, lists all attachments. With `attachmentId`, returns the file.
tags: [Attachments]
parameters:
- name: attachmentId
in: query
schema:
type: string
responses:
'200':
description: Attachment list or file content
post:
summary: Upload an image attachment
tags: [Attachments]
requestBody:
required: true
content:
multipart/form-data:
schema:
type: object
required: [file]
properties:
file:
type: string
format: binary
description: Image file (max 10MB)
responses:
'201':
description: Attachment metadata
content:
application/json:
schema:
$ref: '#/components/schemas/Attachment'
'400':
description: Invalid file
delete:
summary: Delete an attachment
tags: [Attachments]
parameters:
- name: attachmentId
in: query
required: true
schema:
type: string
responses:
'200':
description: Deleted
content:
application/json:
schema:
$ref: '#/components/schemas/Success'
/api/search:
get:
summary: Full-text search
description: Searches across note titles, content, and tags. Excludes trashed notes.
tags: [Search]
parameters:
- name: q
in: query
required: true
schema:
type: string
responses:
'200':
description: Matching notes
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Note'
/api/tags:
get:
summary: List all tags
tags: [Tags]
responses:
'200':
description: Array of tags
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Tag'
/api/sync:
get:
summary: Pull changes since timestamp
tags: [Sync]
parameters:
- name: since
in: query
required: true
schema:
type: integer
description: Unix timestamp in milliseconds
responses:
'200':
description: Notes updated since the given timestamp
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Note'
post:
summary: Push local changes
tags: [Sync]
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [changes]
properties:
changes:
type: array
items:
$ref: '#/components/schemas/SyncChange'
responses:
'200':
description: Changes applied
content:
application/json:
schema:
$ref: '#/components/schemas/Success'
components:
securitySchemes:
sessionCookie:
type: apiKey
in: cookie
name: session
schemas:
Success:
type: object
properties:
success:
type: boolean
Note:
type: object
properties:
id:
type: string
format: uuid
title:
type: string
content:
type: string
color:
type: string
enum: [default, coral, peach, sand, mint, sage, fog, storm, dusk, blossom, clay, chalk]
pinned:
type: boolean
archived:
type: boolean
trashed:
type: boolean
checklistMode:
type: boolean
sortOrder:
type: integer
createdAt:
type: string
format: date-time
updatedAt:
type: string
format: date-time
version:
type: integer
tags:
type: array
items:
type: string
NoteInput:
type: object
properties:
title:
type: string
content:
type: string
color:
type: string
enum: [default, coral, peach, sand, mint, sage, fog, storm, dusk, blossom, clay, chalk]
pinned:
type: boolean
archived:
type: boolean
trashed:
type: boolean
checklistMode:
type: boolean
sortOrder:
type: integer
Tag:
type: object
properties:
id:
type: integer
name:
type: string
Attachment:
type: object
properties:
id:
type: string
noteId:
type: string
filename:
type: string
mimeType:
type: string
size:
type: integer
createdAt:
type: string
format: date-time
SyncChange:
type: object
required: [noteId, operation, timestamp]
properties:
noteId:
type: string
format: uuid
operation:
type: string
enum: [create, update, delete]
timestamp:
type: integer
description: Unix timestamp in milliseconds
data:
$ref: '#/components/schemas/NoteInput'