Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
6e9cb21
chore: vendor v1 OpenAPI spec and generated client
EastSun5566 Sep 23, 2026
f843b84
feat: expose generated raw API entry point
EastSun5566 Sep 23, 2026
db44295
test: cover generated raw API and CI checks
EastSun5566 Sep 23, 2026
6752084
fix: sync note detail ETag contract
EastSun5566 Sep 23, 2026
e09afe2
docs: add generated API reference and local preview
EastSun5566 Sep 23, 2026
7765663
refactor: delegate legacy getNote to generated client
EastSun5566 Sep 23, 2026
88c829d
refactor: delegate legacy getNoteList to generated client
EastSun5566 Sep 23, 2026
880e907
refactor: delegate legacy getHistory to generated client
EastSun5566 Sep 23, 2026
968d8d4
refactor: delegate legacy getTeamNotes to generated client
EastSun5566 Sep 23, 2026
f2ae87c
refactor: delegate legacy getFolderList to generated client
EastSun5566 Sep 23, 2026
f77cfdb
refactor: delegate legacy getTeamFolderList to generated client
EastSun5566 Sep 23, 2026
6781ad5
refactor: delegate legacy folder reads to generated client
EastSun5566 Sep 23, 2026
14a6df9
refactor: delegate legacy folder writes to generated client
EastSun5566 Sep 23, 2026
ba8f6dd
refactor: delegate legacy note updates and deletes to generated client
EastSun5566 Sep 23, 2026
931da04
refactor: delegate legacy note creation to generated client
EastSun5566 Sep 23, 2026
0ff2912
refactor: delegate legacy profile and teams reads to generated client
EastSun5566 Sep 23, 2026
6316aab
refactor: delegate legacy image upload to generated client
EastSun5566 Sep 23, 2026
2832309
fix: emit CommonJS shared chunks with cjs extension
EastSun5566 Sep 24, 2026
043e9ff
feat: add team note detail to API client
EastSun5566 Sep 24, 2026
422507e
feat: add webhook CRUD to API client
EastSun5566 Sep 24, 2026
a543970
feat: add webhook delivery operations to API client
EastSun5566 Sep 24, 2026
6d1962a
feat: add trash operations to API client
EastSun5566 Sep 24, 2026
4c20a1f
feat: add note version operations to API client
EastSun5566 Sep 24, 2026
b5bebe8
feat: add note comment operations to API client
EastSun5566 Sep 24, 2026
f9e5bc9
feat: align API method names with OpenAPI operations
EastSun5566 Sep 24, 2026
fb0ea22
fix: derive public enum types from OpenAPI contract
EastSun5566 Sep 24, 2026
734bc84
fix: isolate retries per API request
EastSun5566 Sep 24, 2026
fd6122c
refactor: derive folder DTOs from OpenAPI types
EastSun5566 Sep 24, 2026
774d7ff
feat: generate OpenAPI operation registry
EastSun5566 Sep 25, 2026
e805f59
chore: polish codegen and type playground
EastSun5566 Sep 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@ jobs:
working-directory: nodejs
run: pnpm install --frozen-lockfile

- name: Check generated client
working-directory: nodejs
run: pnpm check:generated

- name: Typecheck
working-directory: nodejs
run: pnpm typecheck

- name: Run lint
working-directory: nodejs
run: pnpm lint
Expand All @@ -37,6 +45,10 @@ jobs:
working-directory: nodejs
run: pnpm build

- name: Build API reference
working-directory: nodejs
run: pnpm docs:build

- name: Run tests
working-directory: nodejs
env:
Expand Down
55 changes: 55 additions & 0 deletions .github/workflows/pages.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
name: API reference

on:
push:
branches: [master]

permissions:
contents: read

concurrency:
group: github-pages
cancel-in-progress: true

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: pnpm/action-setup@v6
with:
version: 10.33.2

- uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm
cache-dependency-path: nodejs/pnpm-lock.yaml

- name: Install dependencies
working-directory: nodejs
run: pnpm install --frozen-lockfile

- name: Build API reference
working-directory: nodejs
run: pnpm docs:build

- uses: actions/configure-pages@v5

- uses: actions/upload-pages-artifact@v4
with:
path: nodejs/.docs-dist

deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
needs: build
permissions:
pages: write
id-token: write
steps:
- id: deployment
uses: actions/deploy-pages@v4
1 change: 1 addition & 0 deletions nodejs/.eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
src/generated/
1 change: 1 addition & 0 deletions nodejs/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ web_modules/
# Nuxt.js build / generate output
.nuxt
dist
.docs-dist/

# Gatsby files
.cache/
Expand Down
3 changes: 3 additions & 0 deletions nodejs/.stackblitzrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"startCommand": "pnpm build"
}
132 changes: 121 additions & 11 deletions nodejs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ const client = new HackMDAPI('YOUR_ACCESS_TOKEN', 'https://api.hackmd.io/v1', {
```

The client will automatically retry requests that fail with:

- 5xx server errors
- 429 Too Many Requests errors
- Network errors
Expand All @@ -105,16 +106,28 @@ const data = await client.getMe({ unwrapData: true })

### ETag Support

The client supports ETag-based caching for note retrieval. You can pass an ETag to check if the content has changed:
The client can send an ETag to check whether a note changed. The server
generates the ETag and decides whether to return 304; the client does not cache
the note body for you:

```javascript
// First request
const note = await client.getNote('note-id')
const etag = note.etag
const first = await client.getNote('note-id', { unwrapData: false })
const response = await client.getNote('note-id', {
etag: first.headers.etag,
unwrapData: false,
})
// 304 has no body; keep first.data. Otherwise, use response.data.
const note = response.status === 304 ? first.data : response.data
```

// Subsequent request with ETag
const updatedNote = await client.getNote('note-id', { etag })
// If the note hasn't changed, the response will have status 304
Team notes work the same way through the existing client:

```javascript
const teamFirst = await client.getTeamNote('team-path', 'note-id')
const teamResponse = await client.getTeamNote('team-path', 'note-id', {
etag: teamFirst.etag,
})
const teamNote = teamResponse.status === 304 ? teamFirst : teamResponse
```

### Image Upload
Expand All @@ -134,20 +147,117 @@ const uploadedFromNode = await client.uploadNoteImage('note-id', image, {
console.log(uploadedFromNode.data.link)
```

### Webhooks

Manage personal or team webhooks through the same `API` client. Save the secret
returned by `createWebhook` when you create a webhook; later reads do not return it.

```javascript
const webhook = await client.createWebhook({
scope: { type: 'workspace' },
url: 'https://example.com/webhook',
})
console.log(webhook.secret)

const teamWebhooks = await client.listTeamWebhooks('team-path')
```

`listWebhookDeliveries('hook-id', { page: 1, limit: 20 })` returns delivery data
and pagination metadata. `exportWebhookDeliveries('hook-id')` returns
newline-delimited JSON as a string; parse the lines yourself if needed.

### Trash

Use `listTrash()` or `listTeamTrash('team-path')` to find deleted notes.
`restoreNote('note-id')` restores one note; `batchRestore({ noteIds })` reports
success or failure for each note. Pass `{ unwrapData: false }` to inspect the
batch HTTP status (200 or 207).

### Versions

`listVersions('note-id', { named_only: true, page: 1, limit: 20 })` lists saved
versions. Use `getVersion('note-id', versionId)` for content, or
`compareVersions('note-id', { base: 'version:<id>', target: 'note_content' })`
to compare it with the live note. `createVersion` and `updateVersion` accept the
corresponding OpenAPI request bodies.

### Comments

Use `listNoteComments('note-id', { page: 1, limit: 20, threadStatus: 'open' })`
to browse comments. `getNoteComment('note-id', commentId)` reads one comment;
`resolveNoteComment` and `unresolveNoteComment` return the updated comment and
any affected thread.

### Generated Raw API

The `@hackmd/api/raw` entry point exposes every OpenAPI operation as a generated,
one-to-one function. Create a client to share authentication and the API endpoint:

```typescript
import { createClient, getNote } from '@hackmd/api/raw'

const client = createClient({
auth: 'YOUR_ACCESS_TOKEN',
baseURL: 'https://api.hackmd.io/v1',
})

const response = await getNote({
client,
path: { noteId: 'NOTE_ID' },
throwOnError: true,
})

console.log(response.data.content)
```

The package root retains the existing `API` class. Files under
`src/generated` are generated from the vendored OpenAPI document and must not
be edited manually.

On `API`, `listNotes`, `listFolders`, `listTeamNotes`, `listTeamFolders`, and
`listTeams` match the OpenAPI operation names. Existing `getNoteList`,
`getFolderList`, `getTeamNotes`, `getTeamFolderList`, and `getTeams` remain
compatible aliases.

## API

See the [code](./src/index.ts) and [typings](./src/type.ts). The API client is written in TypeScript, so you can get auto-completion and type checking in any TypeScript Language Server powered editor or IDE.
The [API reference](https://hackmdio.github.io/api-client/) covers the existing
`API` class and every raw operation and DTO. To explore autocomplete
without a real token, open the type-only example:

[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/fork/github/hackmdio/api-client/tree/master/nodejs?file=tests/types/playground.mts)

The Pages site is deployed from `master` only.

Run `pnpm docs:dev` from `nodejs` and open `http://127.0.0.1:3000` to preview
the reference locally. It builds the HTML once before serving; rerun the
command after changing source or docs. The output in `.docs-dist` is not
committed.

## Regenerating the raw client

Generation requires Node.js 22.18 or newer.

```bash
pnpm spec:pull
pnpm codegen
pnpm check:generated
```

The OpenAPI document is committed at `spec/hackmd-openapi.json`, and generated
sources are committed under `src/generated` so package builds remain offline and
deterministic.

## E2E tests (live API)

Integration tests call a real HackMD API (staging or production). They are **not** run by `pnpm test` or the default CI job.

**Requirements**
## Requirements

- `HACKMD_ACCESS_TOKEN` — a valid personal access token for the environment you target.
- Optional: `HACKMD_API_ENDPOINT` — defaults to `https://api.hackmd.io/v1`. For staging, use `https://api-stage.hackmd.io/v1`.

**Read-only (default e2e)**
## Read-only (default e2e)

```bash
cd nodejs
Expand All @@ -156,7 +266,7 @@ export HACKMD_API_ENDPOINT=https://api-stage.hackmd.io/v1 # optional
pnpm test:e2e
```

**With CRUD / mutations**
## With CRUD / mutations

Set `HACKMD_E2E_MUTATIONS=1` to run write tests against your account:

Expand Down
40 changes: 40 additions & 0 deletions nodejs/docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# HackMD API for TypeScript

This reference covers the existing `API` class and every generated operation
and type in `@hackmd/api/raw`.

```sh
npm install @hackmd/api
```

## API client

```ts
import { API } from '@hackmd/api'

const client = new API('YOUR_ACCESS_TOKEN')
const note = await client.getNote('NOTE_ID')
console.log(note.content)
```

Existing applications can keep using `API` and its current method signatures.

## Generated raw API

```ts
import { createClient, getNote } from '@hackmd/api/raw'

const client = createClient({ auth: 'YOUR_ACCESS_TOKEN' })
const response = await getNote({ client, path: { noteId: 'NOTE_ID' }, throwOnError: true })
console.log(response.data.content)
```

Use the raw reference for the full OpenAPI operation and DTO catalog.

## Explore the types

The type-only example makes no requests and requires no real token. Open it to
explore autocomplete and type inspection; run real API calls only with your own
token in a trusted local environment.

[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/fork/github/hackmdio/api-client/tree/master/nodejs?file=tests/types/playground.mts)
3 changes: 3 additions & 0 deletions nodejs/jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ const customJestConfig: JestConfigWithTsJest = {
testEnvironment: "node",
transformIgnorePatterns: ["<rootDir>/node_modules/"],
extensionsToTreatAsEsm: [".ts"],
moduleNameMapper: {
"^(\\.{1,2}/.*)\\.js$": "$1",
},
setupFiles: ["dotenv/config"],
testPathIgnorePatterns: ["/node_modules/", "<rootDir>/tests/e2e/"],
}
Expand Down
3 changes: 3 additions & 0 deletions nodejs/jest.e2e.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ const e2eJestConfig: JestConfigWithTsJest = {
testEnvironment: "node",
transformIgnorePatterns: ["<rootDir>/node_modules/"],
extensionsToTreatAsEsm: [".ts"],
moduleNameMapper: {
"^(\\.{1,2}/.*)\\.js$": "$1",
},
setupFiles: ["dotenv/config"],
testMatch: ["<rootDir>/tests/e2e/**/*.spec.ts"],
testTimeout: 60_000,
Expand Down
15 changes: 15 additions & 0 deletions nodejs/openapi-ts.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { defineConfig } from '@hey-api/openapi-ts'

export default defineConfig({
input: './spec/hackmd-openapi.json',
output: {
path: './src/generated',
module: { extension: '.js' },
postProcess: [{ command: 'node', args: ['scripts/generate-operation-registry.mjs'] }],
},
plugins: [
'@hey-api/client-axios',
'@hey-api/typescript',
'@hey-api/sdk',
],
})
Loading
Loading