API.md
Base path: /api. JSON in, JSON out. Auth via session cookie.
Response envelope
Success: { "ok": true, "data": { ... } }
Failure: { "ok": false, "error": { "code": "INSUFFICIENT_ENERGY", "message": "Not enough energy." } }
Status codes
| Code |
Use |
| 200 |
success |
| 201 |
created (register, create hero) |
| 400 |
validation failed (VALIDATION_ERROR) |
| 401 |
not logged in (UNAUTHENTICATED) |
| 403 |
logged in but not allowed (FORBIDDEN, BANNED) |
| 404 |
not found (NOT_FOUND) |
| 409 |
conflict (USERNAME_TAKEN, HERO_EXISTS, EXPEDITION_ACTIVE) |
| 422 |
rule violation (INSUFFICIENT_ENERGY, INSUFFICIENT_GOLD, LEVEL_TOO_LOW, NOT_READY) |
| 429 |
rate limited |
| 500 |
unexpected (never leak stack traces) |
Endpoints
Auth (public unless noted)
| Method |
Path |
Body |
Returns |
| POST |
/auth/register |
{username, password} |
{user:{id,username}} + sets session |
| POST |
/auth/login |
{username, password} |
{user:{id,username}, hasHero} |
| POST |
/auth/logout 🔒 |
– |
{} |
| GET |
/auth/me 🔒 |
– |
{user:{id,username}, hasHero} |
Validation: username 3–20 chars [A-Za-z0-9_]; password 8–72 chars.
Login failure always returns the same generic message (INVALID_CREDENTIALS) — don't reveal whether the username exists.
Character 🔒
| Method |
Path |
Body |
Returns |
| POST |
/character |
{name, class} |
{character} |
| GET |
/character |
– |
{character, derived, equipment, energy:{current,max,nextTickInSec}} |
| POST |
/character/allocate |
{str,agi,int,vit} (points to add) |
{character, derived} |
| POST |
/character/rest |
– |
heals to full for gold cost (see BALANCE) |
Hunting 🔒
| Method |
Path |
Body |
Returns |
| GET |
/zones |
– |
zones with unlocked flag |
| POST |
/hunt |
{zoneId} |
{result:{won,rounds[],xp,gold,loot[],leveledUp,newLevel}, character} |
| GET |
/combat-log?limit=20 |
– |
recent fights |
Inventory 🔒
| Method |
Path |
Body |
Returns |
| GET |
/inventory |
– |
{items:[{inventoryId,item,quantity,equippedSlot}]} |
| POST |
/inventory/equip |
{inventoryId} |
{equipment, derived} |
| POST |
/inventory/unequip |
{slot} |
{equipment, derived} |
| POST |
/inventory/use |
{inventoryId} |
{character, message} |
Shop 🔒
| Method |
Path |
Body |
Returns |
| GET |
/shop |
– |
stock list with prices and canAfford, levelOk |
| POST |
/shop/buy |
{itemId, quantity} (1–99) |
{gold, inventory} |
| POST |
/shop/sell |
{inventoryId, quantity} |
{gold, inventory} |
Expeditions 🔒
| Method |
Path |
Body |
Returns |
| GET |
/expeditions |
– |
available defs + active one (with remainingSec) |
| POST |
/expeditions/start |
{defId} |
{expedition} |
| POST |
/expeditions/claim |
– |
{rewards, character} |
Leaderboard & heroes (public read)
| Method |
Path |
Query |
Returns |
| GET |
/leaderboard |
`type=level |
gold |
| GET |
/heroes/:name |
– |
public profile (name, class, level, kills, equipped item names) |
Misc
| GET | /health | – | {ok:true} |
Rules for every route
- Validate with zod; on failure → 400
VALIDATION_ERROR with field messages.
- Never trust client-supplied ids for ownership: always scope by
req.session.userId → character.
- Responses never include
password_hash or session data.
- State-changing endpoints are POST. GET must never modify state.
- Return the updated character where relevant so the UI can refresh without a second call.