Our Shopping List 5.0.0 API 使用详解

Our Shopping List 5.0.0 REST API 全 20 个端点实测详解(boards/lists/items 三域 CRUD、403 门控、201 自动建板、级联删除、Socket.IO 实时通道握手),含 curl 示例与真实响应回显。

Our Shopping List自托管REST API2026/10/24

Our Shopping List 5.0.0 API 使用详解

目标:把 Our Shopping List(OSL)5.0.0 对外暴露的接口面讲透——REST 三域(面板/清单/条目)20 个端点逐个给出 curl 示例与真实响应回显,实时通道 Socket.IO 的握手与参数实测,以及错误码、门控开关与常见坑。全部示例已在 Linux 服务器实测通过(服务地址 http://127.0.0.1:8132,2026-10-24),文中 JSON 均为实际回显(个别长输出标注「节选」)。

一、环境信息

  • 服务:OSL 5.0.0(ourshoppinglist/our-shopping-list:5.0.0,Node.js 22.23.4)
  • 数据库栈:FerretDB 2.7 + PostgreSQL 17(对 API 透明,仅影响持久化层)
  • 服务地址:http://127.0.0.1:8132(宿主机端口 8132 → 服务监听 8080)
  • 实测日期:2026-10-24;文中出现的面板/清单/条目 ID 为实测环境中的真实值(对照测试时请换成自己环境的 ID)

二、前置条件

  • 服务已部署且健康检查通过:curl http://127.0.0.1:8132/healthcheck 返回 {"success":true}(部署见《Our Shopping List Docker Compose 部署指南》)。
  • 客户端工具:任意 HTTP 客户端即可,本文用 curl(7.88+);实时通道调试用浏览器 DevTools 的 WebSocket 面板或 curl 手工握手(见第六章)。
  • 认证模型(选型/接入前必读):OSL 无任何内置认证——没有账号、没有 API Key、没有 Token。任何能访问该端口的对象都可读可写。安全边界是网络位置:内网/VPN 可信环境下直接用;跨边界暴露必须在上游反代加认证层(做法见《Our Shopping List 5.0.0 介绍与选型指南》2.4 节对比)。
  • 限流约定:无。家用定位没有限流/配额设计,批量脚本请自控节奏(对同一清单高频写入会放大 Socket.IO 广播压力)。

三、API 总览

维度约定
协议与风格HTTP/1.1,REST 风格 JSON;无版本前缀(升级大版本可能破坏兼容,见 5.x 变更记录)
基础路径服务根路径,无 /api 前缀;子路径部署时挂在 BASE_URL 下
请求体application/json;条目数量字段为 qty(数值型,不是 quantity 字符串,实测会归一化为整数)
实体标识UUID 字符串(id 字段),URL 直接使用;面板另有唯一 slug 可作 URL 定位符
认证无
分页无——所有列表端点全量返回
排序/过滤服务端不做;客户端自行过滤(唯一例外:已勾选条目按勾选字段排序,行为由前端配置决定)
错误响应统一形状 {"error":{"message":"<原因>"}},配合 HTTP 状态码(403/404/501 等)
实时通道Socket.IO(Engine.IO v4),路径 /socket.io/,详见第六章

端点清单(20 个,按域分组;★ = 本文实测覆盖):

域方法与路径说明
实用GET /healthcheck ★健康检查(镜像内置探针同款)
实用GET /config.js ★前端运行时配置注入(详见《Our Shopping List 5.0.0 配置参数详解》第三章)
面板GET /boards ★列出全部面板(默认 403,门控)
面板GET /boards/:boardId ★按 ID 取面板
面板GET /boards/by-slug/:slug ★按 slug 取面板(不存在时自动创建,201)
面板GET /boards/:boardId/lists ★列出面板下清单(桩端点,501)
面板GET /boards/by-slug/:slug/lists同上(桩端点)
面板DELETE /boards/:boardId ★删除面板(默认 403,门控)
清单GET /lists ★列出全部清单(生产模式 403)
清单GET /lists/:listId ★按 ID 取清单(含 items 明细)
清单HEAD /lists/:listId ★探测清单最后修改时间
清单POST /lists ★新建清单
清单PATCH /lists/:listId ★改清单(当前仅 name)
清单DELETE /lists/:listId ★删清单(级联删除条目)
条目GET /items ★列出全部条目(生产模式 403)
条目GET /lists/:listId/items ★列出清单下条目
条目POST /items ★新建条目(全局端点,body 带 listId)
条目POST /lists/:listId/items ★新建条目(清单子路径端点)
条目PATCH /items/:itemId ★改条目(勾选/数量/备注等)
条目DELETE /items/:itemId ★删条目

四、认证与首次调用

OSL 没有认证流程,「首次调用」就是一次健康检查加一次面板定位:

1)健康检查:

curl http://127.0.0.1:8132/healthcheck

实测回显:

{"success":true}

2)按 slug 定位面板(面板的对外入口):

curl http://127.0.0.1:8132/boards/by-slug/family

实测回显(输出节选):

{"_id":"5933a4b6-c2c6-4969-ba3c-061a3770bf13","id":"5933a4b6-c2c6-4969-ba3c-061a3770bf13","name":"family","slug":"family","lists":[{"id":"4ff671d0-9517-45a1-bd7a-c3c8cdf9d18b","name":"周六大采购","listIndex":0,...}]}

⚠️ 注意:这是 OSL API 最反直觉的一处——GET /boards/by-slug/:slug 对不存在的 slug 会直接创建面板并返回 201(首次实测:GET /boards/by-slug/nope 后系统里多出了 nope 面板)。这是产品语义(UI 上「访问即建」)而不是 bug,但也意味着脚本里写错 slug 不会报错,而是悄悄新建空面板。排查时用 board:find CLI 核对面板清单(见《Our Shopping List Docker Compose 部署指南》10.1 节)。

五、各 API 域详解

5.1 面板域(boards)

GET /boards —— 列出全部面板(默认关闭)

curl -s http://127.0.0.1:8132/boards -w "\n[HTTP %{http_code}]\n"

实测回显(默认门控下):

{"error":{"message":"Not available! See VITE_APP_LIST_ALL_BOARDS_ENABLED."}}
[HTTP 403]

这是刻意的安全默认(无认证实例不允许枚举面板)。确需开放时设 VITE_APP_LIST_ALL_BOARDS_ENABLED=1 重启(实测打开后返回面板数组)。

GET /boards/:boardId —— 按 ID 取面板

curl -s http://127.0.0.1:8132/boards/5933a4b6-c2c6-4969-ba3c-061a3770bf13 -w "\n[HTTP %{http_code}]\n"

实测回显(输出节选):

{"_id":"5933a4b6-...","id":"5933a4b6-...","name":"family","slug":"family","createdAt":"2026-10-24T02:02:11.087Z","lastUpdatedAt":"2026-10-24T02:02:11.087Z"}
[HTTP 200]

GET /boards/:boardId/lists 与 GET /boards/by-slug/:slug/lists —— 桩端点(501)

curl -s http://127.0.0.1:8132/boards/5933a4b6-c2c6-4969-ba3c-061a3770bf13/lists -w "\n[HTTP %{http_code}]\n"

实测回显:

[HTTP 501]

两个端点路由存在但未实现,服务端无响应体。要取面板下清单,用 GET /boards/by-slug/:slug(响应内嵌 lists 数组)或 GET /lists/:listId。

DELETE /boards/:boardId —— 删除面板(默认 403)

curl -s -X DELETE http://127.0.0.1:8132/boards/5933a4b6-c2c6-4969-ba3c-061a3770bf13 -w "\n[HTTP %{http_code}]\n"

实测回显(默认门控下):

{"error":{"message":"Not available! See VITE_APP_BOARD_DELETION_ENABLED."}}
[HTTP 403]

VITE_APP_BOARD_DELETION_ENABLED=1 重启后,同一请求实测:

{"_id":"...","id":"...","name":"超市采购","slug":"mart",...}
[HTTP 200]

删除为级联:面板下全部清单与条目一并消失。门控说明与风险提示见《Our Shopping List 5.0.0 配置参数详解》5.5 节。

5.2 清单域(lists)

POST /lists —— 新建清单

curl -s -X POST http://127.0.0.1:8132/lists \
  -H 'Content-Type: application/json' \
  -d '{"name":"周六采购","boardId":"5933a4b6-c2c6-4969-ba3c-061a3770bf13"}' \
  -w "\n[HTTP %{http_code}]\n"

实测回显:

{"_id":"4ff671d0-9517-45a1-bd7a-c3c8cdf9d18b","id":"4ff671d0-9517-45a1-bd7a-c3c8cdf9d18b","name":"周六采购","boardId":"5933a4b6-c2c6-4969-ba3c-061a3770bf13","listIndex":0,"createdAt":"2026-10-24T02:05:40.899Z","lastUpdatedAt":"2026-10-24T02:05:40.899Z","userContext":null}
[HTTP 201]

要点:boardId 必填(目标面板);listIndex 为面板内排序号,不传自动追加。

GET /lists/:listId —— 清单详情(含条目)

curl -s http://127.0.0.1:8132/lists/4ff671d0-9517-45a1-bd7a-c3c8cdf9d18b -w "\n[HTTP %{http_code}]\n"

实测回显(输出节选):

{"_id":"4ff671d0-...","id":"4ff671d0-...","name":"周六大采购","boardId":"5933a4b6-...","items":[{"id":"b6681e56-...","name":"牛奶","qty":3,"checked":true,...},{"id":"55bddd76-...","name":"全麦面包","qty":1,"checked":false,...}],"listIndex":0}
[HTTP 200]

HEAD /lists/:listId —— 探测最后修改时间

curl -I -X HEAD http://127.0.0.1:8132/lists/4ff671d0-9517-45a1-bd7a-c3c8cdf9d18b

实测回显(完整响应头):

HTTP/1.1 204 No Content
Last-Modified-Iso: 2026-10-24T02:11:40.196Z
Osl-Entity-Type: List
Osl-Entity-Id: 4ff671d0-9517-45a1-bd7a-c3c8cdf9d18b

这是 OSL 的轻量变更探测端点(Last-Modified-Iso 是自定义头,非标准 Last-Modified;附加 Osl-Entity-Type/Id 标识实体)。适合做「低频轮询 + 有变更再拉详情」的脚本模式。

PATCH /lists/:listId —— 改清单名(当前仅 name 一个字段)

curl -s -X PATCH http://127.0.0.1:8132/lists/4ff671d0-9517-45a1-bd7a-c3c8cdf9d18b \
  -H 'Content-Type: application/json' \
  -d '{"name":"周六大采购"}' -w "\n[HTTP %{http_code}]\n"

实测回显:

{"_id":"4ff671d0-...","id":"4ff671d0-...","name":"周六大采购",...}
[HTTP 200]

DELETE /lists/:listId —— 删清单(级联)

curl -s -X DELETE http://127.0.0.1:8132/lists/d63ada22-6e98-499a-bfa0-2e5105bc222c -w "\n[HTTP %{http_code}]\n"

实测回显(输出节选):

{"_id":"d63ada22-...","id":"d63ada22-...","name":"待清理","boardId":"5933a4b6-...","items":[...]}
[HTTP 200]

返回被删清单全文(含其下条目,方便核对)。级联删除:清单下条目同步清除,不可恢复——批量清理脚本里这是效率工具也是大杀器。

错误码实测:

请求实测回显状态码
GET /lists/00000000-0000-0000-0000-00000000dead{"error":{"message":"List not found"}}404
GET /lists/(空 ID,落入列表路由){"error":{"message":"Not available in production mode!"}}403
GET /lists(生产模式列全部){"error":{"message":"Not available in production mode!"}}403

5.3 条目域(items)

POST /lists/:listId/items —— 清单下新建条目(推荐入口)

curl -s -X POST http://127.0.0.1:8132/lists/4ff671d0-9517-45a1-bd7a-c3c8cdf9d18b/items \
  -H 'Content-Type: application/json' \
  -d '{"name":"鸡蛋","qty":2}' -w "\n[HTTP %{http_code}]\n"

实测回显:

{"_id":"b6681e56-e64e-44fa-8ac4-3ac0a5da7471","id":"b6681e56-e64e-44fa-8ac4-3ac0a5da7471","name":"鸡蛋","qty":2,"checked":false,"createdAt":"2026-10-24T02:10:19.499Z","lastUpdatedAt":"2026-10-24T02:10:19.499Z","createdById":"e09a62b6-3ea5-45c5-a1b7-c9c04ba8bb28","listId":"4ff671d0-9517-45a1-bd7a-c3c8cdf9d18b","boardId":"5933a4b6-c2c6-4969-ba3c-061a3770bf13","doneById":null,"doneAt":null,"orderedItem":{},"userContext":null,"itemIndex":2,"indentLevel":0}
[HTTP 201]

字段实测要点:

  • qty 是数值;传字符串(如 "2 盒")不会报错但被归一化为 1(实测教训)——数量语义请用数值 + details 承载单位文字;
  • createdById 自动落为会话用户上下文(无认证实例为内置默认用户);
  • itemIndex/indentLevel 支持排序与缩进层级(UI 拖拽能力的底层字段)。

POST /items —— 全局新建条目

curl -s -X POST http://127.0.0.1:8132/items \
  -H 'Content-Type: application/json' \
  -d '{"name":"孤儿条目","listId":"4ff671d0-9517-45a1-bd7a-c3c8cdf9d18b"}' -w "\n[HTTP %{http_code}]\n"

实测回显(输出节选):

{"id":"e3e25c31-...","name":"孤儿条目","qty":1,...,"listId":"4ff671d0-..."}
[HTTP 201]

body 里带 listId 时行为与子路径端点一致;不带则进入无清单归属的孤儿空间(产品内置的「待归位」机制,UI 侧可拖回清单)。

GET /lists/:listId/items —— 列出清单条目

curl -s http://127.0.0.1:8132/lists/4ff671d0-9517-45a1-bd7a-c3c8cdf9d18b/items -w "\n[HTTP %{http_code}]\n"

实测回显(输出节选):

[{"id":"b6681e56-...","name":"牛奶","qty":3,"checked":true,...},{"id":"55bddd76-...","name":"全麦面包","qty":1,"checked":false,...}]
[HTTP 200]

全量返回、无分页;无服务端过滤参数(前端本地过滤)。

PATCH /items/:itemId —— 改条目(勾选/数量/备注)

curl -s -X PATCH http://127.0.0.1:8132/items/b6681e56-e64e-44fa-8ac4-3ac0a5da7471 \
  -H 'Content-Type: application/json' \
  -d '{"qty":3,"checked":true}' -w "\n[HTTP %{http_code}]\n"

实测回显(输出节选):

{"id":"b6681e56-...","name":"牛奶","qty":3,"checked":true,"doneAt":"2026-10-24T02:09:04.204Z",...}
[HTTP 200]

要点:checked 置 true 时服务端自动落 doneAt/doneById(勾选审计字段);checked 为 false 时自动清空。这是「勾选」动作的 API 形态。

DELETE /items/:itemId —— 删条目

curl -s -X DELETE http://127.0.0.1:8132/items/b6681e56-e64e-44fa-8ac4-3ac0a5da7471 -w "\n[HTTP %{http_code}]\n"

实测回显:

{"_id":"b6681e56-...","id":"b6681e56-...","name":"鸡蛋","qty":2,...}
[HTTP 200]

对已删除的 ID 再删一次,实测回显:

{"error":{"message":"Item not found"}}
[HTTP 404]

(删一次成功返回条目全文,二次删除 404——脚本做幂等重试时以此为准。)

5.4 实用端点与错误约定

  • GET /healthcheck:固定 {"success":true}(HTTP 200)。镜像内置健康探针(每分钟 wget 同一端点),编排健康状态与它联动。
  • GET /config.js:动态 JS,Object.assign(window, {...}) 注入 35 个配置键(实测)。脚手架脚本可解析它确认运行时配置。
  • 错误形状:全部为 {"error":{"message":"<英文原因>"}};403 常伴提示具体门控变量名,404 为实体不存在,501 为上游未实现的桩端点。

六、实时通道(Socket.IO)

OSL 的多人实时同步(任何端增删改,其他端秒级更新)走 Socket.IO,路径 /socket.io/。REST 改动后不需要订阅也能生效,订阅的价值是省掉轮询。

握手参数(polling 方式实测,Engine.IO v4):

curl -s "http://127.0.0.1:8132/socket.io/?EIO=4&transport=polling"

实测回显:

0{"sid":"VoPd6--KC1fldQwbAAAA","upgrades":["websocket"],"pingInterval":25000,"pingTimeout":20000,"maxPayload":1000000}

WebSocket 升级(实测 101):

KEY=$(head -c 16 /dev/urandom | base64)
curl -si "http://127.0.0.1:8132/socket.io/?EIO=4&transport=websocket" \
  -H "Connection: Upgrade" -H "Upgrade: websocket" \
  -H "Sec-WebSocket-Version: 13" -H "Sec-WebSocket-Key: $KEY" --max-time 3 | head -4

实测回显:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: XitCrcQ8ObF9gyKOxaszQAsqD6o=

参数解读与运维要点:

参数实测值说明
pingInterval / pingTimeout25000 / 20000 ms心跳节奏,对应配置 VITE_APP_SOCKETIO_PING_INTERVAL/TIMEOUT
maxPayload1 MB单帧上限
connectionStateRecovery1800000 ms断线 30 分钟内重连恢复会话(VITE_APP_SOCKETIO_CSR_MAXDISCONNECTIONDURATION)
反代要求—必须透传 Upgrade/Connection 头,否则自动回落 polling 或断流(Nginx 配置见《Our Shopping List Docker Compose 部署指南》Q3)

服务端启动日志的对照证据(DEBUG=socket.io:server,express-urlrewrite 实测):

socket.io:server creating engine.io instance with opts {"path":"/socket.io/","pingInterval":25000,"pingTimeout":20000,"connectionStateRecovery":{"maxDisconnectionDuration":1800000,...}}
Serving socket-io on /socket.io/

💡 技术拓展:WebSocket 常见故障对照——

现象原因处置
握手 400/404反代未透传升级头补 proxy_set_header Upgrade/Connection
连接频繁断开重连中间设备掐空闲长连接调小 VITE_APP_SOCKETIO_PING_INTERVAL
只能 polling 无 ws反代或网关不支持 HTTP/1.1 升级放行或接受轮询降级(功能不丢,实时性变差)

客户端集成用任何 Socket.IO 客户端库(io(<服务器地址>),默认路径 /socket.io/),事件流以「清单/条目实体变更」为主题,跟随前端实现;纯脚本集成推荐「REST 写 + HEAD 探测」的组合(见 5.2 节 HEAD 端点)。

七、常见问题

Q1:GET /boards / GET /lists / GET /items 全部 403,服务是不是没配好? 正常。列表枚举端点默认全部关闭(无认证实例防侦察):/lists 与 /items 在生产模式固定 403(Not available in production mode!),/boards 受 VITE_APP_LIST_ALL_BOARDS_ENABLED 门控。数据获取走 by-slug 面板端点或清单详情端点(均开放)。

Q2:POST 创建条目时 qty 传了字符串没生效? qty 仅接受数值(实测传 "2 盒" 被归一化成 1)。单位/描述放 details。

Q3:脚本里 slug 打错,为什么没报错反而多了一个面板? GET /boards/by-slug/:slug 语义就是「不存在则创建」(实测 201)。写脚本前先固化面板 slug 列表,或用 CLI board:find 做前置校验。

Q4:想给 API 加个 API Key? OSL 当前版本不支持任何应用层认证,没有预留 Key 机制。需要边界安全只能在外部加:反代 Basic Auth / OAuth2-Proxy / 网关鉴权,或干脆只在内网/VPN 可达。

Q5:脚本写入了数据,手机上页面没更新? 页面依赖 Socket.IO 长连接推送;手机端若被系统挂起(切后台),恢复时靠 30 分钟内的会话恢复窗口拉取增量,超时则整页刷新。脚本侧无需处理,这是端侧行为。

Q6:有没有 OpenAPI/Swagger 文档? 没有官方 OpenAPI 描述文件。接口面以本文与源码路由(server/src/*/routes.js)为准;大版本升级(如 4.x→5.x)时对照 Release 说明核对兼容性。

八、参考链接