diff --git a/nodejs/README.md b/nodejs/README.md
index 56b7d94..2e93e1b 100644
--- a/nodejs/README.md
+++ b/nodejs/README.md
@@ -117,6 +117,23 @@ const updatedNote = await client.getNote('note-id', { etag })
// If the note hasn't changed, the response will have status 304
```
+### Image Upload
+
+Upload an image to a note with `uploadNoteImage`. The API returns the uploaded image link in `data.link`.
+
+```javascript
+// Browser: pass a File from an
+const uploaded = await client.uploadNoteImage('note-id', file)
+console.log(uploaded.data.link)
+
+// Node.js 18+: pass a Blob and optional filename
+const image = new Blob([imageBuffer], { type: 'image/png' })
+const uploadedFromNode = await client.uploadNoteImage('note-id', image, {
+ filename: 'diagram.png'
+})
+console.log(uploadedFromNode.data.link)
+```
+
## 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.
@@ -143,7 +160,7 @@ npm run test:e2e
Set `HACKMD_E2E_MUTATIONS=1` to run write tests against your account:
-- **Notes:** create → get → update (title, content, tags) → list → delete.
+- **Notes:** create → get → update (title, content, tags) → upload fixture image → list → delete.
- **Folders:** one integration test runs create (root + nested) → get → update → list → folder-order round-trip (skipped if that API returns 404) → delete. If **POST `/folders`** returns 404 (common before full production rollout), the test exits early with a warning; use staging or `HACKMD_E2E_FOLDERS=0`.
```bash
diff --git a/nodejs/src/index.ts b/nodejs/src/index.ts
index 3389ee4..1ea9bb7 100644
--- a/nodejs/src/index.ts
+++ b/nodejs/src/index.ts
@@ -26,6 +26,8 @@ import {
UpdateFolderOrderBody,
UpdateTeamFolderBody,
UpdateUserFolderBody,
+ UploadNoteImageOptions,
+ UploadNoteImageResponse,
} from './type'
import * as HackMDErrors from './error'
@@ -70,9 +72,6 @@ export class API {
this.axios = axios.create({
baseURL: hackmdAPIEndpointURL,
- headers:{
- "Content-Type": "application/json",
- },
timeout: options.timeout
})
@@ -209,6 +208,16 @@ export class API {
return this.unwrapData(this.axios.delete(`notes/${noteId}`), options.unwrapData) as unknown as OptionReturnType
}
+ async uploadNoteImage (noteId: string, image: Blob, options = defaultOption as Opt): Promise> {
+ const formData = new FormData()
+ formData.append('image', image, options.filename ?? undefined)
+
+ return this.unwrapData(
+ this.axios.post(`notes/${noteId}/images`, formData),
+ options.unwrapData,
+ ) as unknown as OptionReturnType
+ }
+
async getTeams (options = defaultOption as Opt): Promise> {
return this.unwrapData(this.axios.get("teams"), options.unwrapData) as unknown as OptionReturnType
}
diff --git a/nodejs/src/type.ts b/nodejs/src/type.ts
index 50eb3f3..9dd93e8 100644
--- a/nodejs/src/type.ts
+++ b/nodejs/src/type.ts
@@ -114,6 +114,17 @@ export type CreateUserNote = SingleNote
export type UpdateUserNote = void
export type DeleteUserNote = void
+export type UploadNoteImageResponse = {
+ data: {
+ link: string
+ }
+}
+
+export type UploadNoteImageOptions = {
+ unwrapData?: boolean
+ filename?: string
+}
+
// Teams
export type GetUserTeams = Team[]
diff --git a/nodejs/tests/api.spec.ts b/nodejs/tests/api.spec.ts
index 9f3f28f..998c8c0 100644
--- a/nodejs/tests/api.spec.ts
+++ b/nodejs/tests/api.spec.ts
@@ -143,6 +143,41 @@ test('updateFolderOrder sends order payload', async () => {
})
})
+test('uploadNoteImage sends image as multipart form data', async () => {
+ let uploaded: FormDataEntryValue | null = null
+ let contentType: string | null = null
+
+ server.use(
+ http.post('https://api.hackmd.io/v1/notes/test-note-id/images', async ({ request }) => {
+ contentType = request.headers.get('content-type')
+ const formData = await request.formData()
+ uploaded = formData.get('image')
+
+ return HttpResponse.json({
+ data: {
+ link: 'https://hackmd.io/_uploads/test-image.png',
+ },
+ })
+ }),
+ )
+
+ const response = await client.uploadNoteImage(
+ 'test-note-id',
+ new Blob(['test image'], { type: 'image/png' }),
+ { filename: 'test-image.png' },
+ )
+
+ expect(contentType).toContain('multipart/form-data')
+ expect(uploaded).toBeInstanceOf(Blob)
+ const uploadedBlob = uploaded as unknown as Blob
+ expect(uploadedBlob.type).toBe('image/png')
+ expect(response).toEqual({
+ data: {
+ link: 'https://hackmd.io/_uploads/test-image.png',
+ },
+ })
+})
+
test('should support updating team note title and tags metadata', async () => {
const updatedTags = ['team', 'metadata']
let requestBody: unknown
diff --git a/nodejs/tests/e2e/api.e2e.spec.ts b/nodejs/tests/e2e/api.e2e.spec.ts
index 8729e6b..8a350fb 100644
--- a/nodejs/tests/e2e/api.e2e.spec.ts
+++ b/nodejs/tests/e2e/api.e2e.spec.ts
@@ -1,3 +1,4 @@
+import { readFileSync } from 'fs'
import type { ApiFolderOrder } from '../../src'
import { API } from '../../src'
import { HttpResponseError } from '../../src/error'
@@ -144,6 +145,19 @@ describe('HackMD API (live e2e)', () => {
}
})
+ it('uploadNoteImage uploads an image to the note', async () => {
+ const image = readFileSync('tests/fixtures/hackmd-cute-logo.png')
+
+ const uploaded = await client.uploadNoteImage(
+ noteId,
+ new Blob([new Uint8Array(image)], { type: 'image/png' }),
+ { filename: `hackmd-cute-logo-${stamp}.png` },
+ )
+
+ expect(uploaded.data.link).toEqual(expect.any(String))
+ expect(uploaded.data.link.length).toBeGreaterThan(0)
+ })
+
it('getNoteList includes the note', async () => {
const list = await client.getNoteList()
const found = list.find(n => n.id === noteId)
diff --git a/nodejs/tests/fixtures/hackmd-cute-logo.png b/nodejs/tests/fixtures/hackmd-cute-logo.png
new file mode 100644
index 0000000..8d20cd0
Binary files /dev/null and b/nodejs/tests/fixtures/hackmd-cute-logo.png differ