尚未检测到更新。GitHub 发布版本后会显示在这里。
个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
在项目文件夹中运行 /nomorevibe verify,即可验证所有权并自行管理介绍。
一分钟完成验证制作者的介绍仅有上面的一句话。 更多详情请阅读仓库 README。 仓库 README ↗
正在加载完整 README。
查看完整 README ↗An API that lets patients book clinic appointment slots, plus a web page that uses it. Built to the Koya assessment spec, levels 1–5.
pg)/; the API is at the root paths belowEvery request needs X-API-Key. Every error is {"error": "<message>"}.
| Method | Path | Level | Notes |
|---|---|---|---|
POST | /reset | 1 | Deletes every booking, replaces all slots |
GET | /slots?provider= | 1 | Earliest first, with available |
POST | /bookings | 1–5 | 201 booking · 400 · 404 · 409 · 422 · 429 |
GET | /bookings/{id} | 1 | 404 if unknown |
DELETE | /bookings/{id} | 3 | Cancels and frees the slot · 404 · 409 if already cancelled |
GET | /bookings?email=&limit=&cursor= | 4 | Oldest first, cursor paging, limit 1–50 (default 10) |
Level 1. Slots and bookings live in Postgres, so they persist across requests and
serverless instances. Slot start/end are echoed back exactly as sent and ordered by
their parsed timestamps.
Level 2. Auth runs before the body is parsed, so a bad key is always 401, and keys
are compared in constant time. Bookings are rejected with 400 for missing, blank or
non-string fields, an invalid email, or a slot that has already started; 404 for an
unknown slot. name and email are trimmed and email is stored in lower case.
Level 3. A partial unique index allows only one confirmed booking per slot:
CREATE UNIQUE INDEX bookings_one_confirmed_per_slot ON bookings (slot_id) WHERE status = 'confirmed';
Postgres enforces it, so it holds however many requests (or serverless instances) race;
the loser's unique violation becomes 409. The slot check is part of the same INSERT,
so there's no check-then-insert gap. Cancelling is a single conditional UPDATE, which
drops the booking out of the index and frees the slot. Tested with 50 simultaneous
requests for one slot: exactly one 201.
Level 4. Responses are stored per Idempotency-Key with a hash of the body (key order
ignored). Each request takes a Postgres advisory lock on its key for the length of its
transaction, so simultaneous retries queue up: the first books, the rest replay its
stored response (same status, same booking id). Error responses are replayed too. A
reused key with a different body gets 422. Booking lists use an opaque cursor over
insertion order; the last page has next_cursor: null.
Level 5. A sliding 60-second window per email, stored in Postgres. A per-email
advisory lock makes check-and-record atomic, so a burst of 20 simultaneous requests lets
exactly 5 through. Failed attempts (validation errors, unknown slots, 409s) count. The
6th gets 429 with Retry-After in whole seconds, computed from the oldest attempt in
the window. Rejected attempts aren't recorded, so Retry-After is accurate.
/reset also clears stored idempotent responses and rate-limit counters, since
they refer to bookings that no longer exist.API_KEY, the server accepts a second WEB_KEY that is injected into the
page and can do everything except POST /reset (which returns 403 for it).
API_KEY stays private. The per-email rate limit still applies to the page.npm install
cp .env.example .env # fill in DATABASE_URL, API_KEY, WEB_KEY
export $(cat .env | xargs)
npm start # http://localhost:3000
npm run seed # demo slots for the next two weeks
The schema is created automatically on the first request.
# against a local database (starts the app in-process)
DATABASE_URL=... API_KEY=... npm test
# against the live deployment
BASE_URL=https://your-app.vercel.app API_KEY=... npm test
# add SLOW=1 to also wait out a rate-limit window (~65s)
The tests call /reset, which wipes all data. Re-run npm run seed afterwards if you
want demo slots back on the page.
iad1). Copy the pooled
connection string (host contains -pooler).DATABASE_URL: the Neon pooled stringAPI_KEY: a long random value (node -e "console.log(require('crypto').randomBytes(24).toString('base64url'))")WEB_KEY: a different random valuecurl https://your-app.vercel.app/slots -H "X-API-Key: $API_KEY" # {"slots":[]}
BASE_URL=https://your-app.vercel.app API_KEY=$API_KEY npm test
BASE_URL=https://your-app.vercel.app API_KEY=$API_KEY npm run seed
API_KEY. After submitting, don't redeploy or change env vars.api/index.js Vercel entry point
server.js Local entry point
src/app.js Routes
src/db.js Pool, schema, transactions
src/auth.js X-API-Key / WEB_KEY check
src/validate.js Booking validation
src/idempotency.js Idempotency-Key storage and locking
src/rateLimit.js Per-email sliding window
web/index.html Patient web page (not public/: Vercel would serve that raw)
scripts/seed.js Demo slots
test/api.test.js Spec tests