ryOS API Design Guide
This document outlines the current patterns and conventions used in the ryOS API.
Architecture Overview
The API is implemented as Node.js route handlers under api/. The default implementation pattern is the shared apiHandler wrapper, which centralizes:
- CORS preflight and response headers
- origin allowlist checks
- method allowlists
- optional JSON body handling
- optional Zod request-body validation
- optional/required auth resolution
- logger setup and consistent fallback error handling
Auth boundaries are unified through request-auth.ts, which enforces paired auth headers.
File Structure
api/
├── _utils/
│ ├── api-handler.ts # Primary route wrapper
│ ├── request-auth.ts # Unified auth extraction + token validation
│ ├── redis.ts # Redis client factory (Upstash REST + standard Redis backends)
│ ├── storage.ts # S3 compatible object storage adapter
│ ├── contacts.ts # Contacts state read/write helpers
│ ├── _cors.ts # Origin policy + CORS helpers
│ ├── _rate-limit.ts # Counter-based rate limit utilities
│ ├── _validation.ts # Input/profanity/escaping helpers
│ ├── _ssrf.ts # Safe URL fetch + SSRF protections
│ ├── _logging.ts # Request-scoped logger
│ ├── _aiModels.ts # AI model registry and factory
│ └── _aiPrompts.ts # Shared AI prompt templates
├── auth/ # Auth/session endpoints
├── chat/ # Main AI chat endpoint + tool definitions
├── rooms/ # Chat rooms/messages/presence
├── listen/ # Listen Together session APIs
├── songs/ # Song library + lyric processing
├── ai/ # Memory processing + room AI reply
├── sync/ # Sync v2 journal, blob, preference, and maintenance endpoints
├── telegram/ # Telegram account linking
└── ... # Other feature routes (stocks, speech, etc.)
Naming Conventions
_helpers/for domain-specific internal helpers
_utils/for globally shared API utilities_*.tsfiles for internal/private modulesindex.tsfor collection routes[id].tsand nested dynamic route folders for path params
Common Patterns
1. apiHandler Wrapper (Primary Pattern)
import { apiHandler } from "../_utils/api-handler.js";
import { z } from "zod";
const RequestSchema = z.object({
name: z.string().min(1),
});
export default apiHandler<z.infer<typeof RequestSchema>>(
{
methods: ["GET", "POST"],
auth: "optional", // "none" | "optional" | "required"
allowExpiredAuth: false, // optional
parseJsonBody: true, // optional; implied when bodySchema is set
bodySchema: RequestSchema, // optional Zod validation + typed body
contentType: "application/json", // optional, null disables default header
},
async ({ req, res, redis, logger, startTime, origin, user, body }) => {
// business logic
logger.response(200, Date.now() - startTime);
res.status(200).json({ ok: true });
}
);
apiHandler provides:
OPTIONShandling (204) with endpoint-specific method list
- consistent
403for disallowed origins - consistent
405for unsupported methods - Redis client injection via
createRedis() - auth resolution via
resolveRequestAuth() - fallback
500JSON error guard
2. Endpoint Function Signature
When using apiHandler, handlers receive:
req,res
redislogger,startTimeoriginuser(nullor resolved auth user)body(nullor parsed JSON whenparseJsonBody: true)
3. Auth Boundaries via request-auth
resolveRequestAuth() enforces a shared policy:
- required auth endpoints need both headers:
Authorization: Bearer <token>X-Username: <username>
- required endpoints return
400for partial credentials and401for invalid token/username pairs - optional-auth endpoints allow anonymous requests; partial, stale, or invalid credentials are ignored and resolve to
user: null
For non-apiHandler routes (legacy/multipart), call resolveRequestAuth() directly to keep behavior aligned.
4. Rate Limiting
Use _utils/_rate-limit.ts primitives:
import * as RateLimit from "../_utils/_rate-limit.js";
import { getClientIp } from "../_utils/_rate-limit.js";
const ip = getClientIp(req);
const key = RateLimit.makeKey(["rl", "feature", "burst", "ip", ip]);
const result = await RateLimit.checkCounterLimit({
key,
windowSeconds: 60,
limit: 30,
});
if (!result.allowed) {
res.setHeader("Retry-After", String(result.resetSeconds));
return res.status(429).json({
error: "rate_limit_exceeded",
limit: result.limit,
retryAfter: result.resetSeconds,
});
}
5. Response & Error Handling
The preferred API shape is JSON for standard requests and SSE for streaming endpoints.
- success: explicit payloads (
{ success: true },{ data: ... }, etc.)
- client errors:
400/401/403/404/405/429with JSON{ error: "..." } - server errors:
500with JSON{ error: "..." }
For SSE endpoints, set stream headers explicitly and send structured events (start, line, complete, error, etc.).
6. Manual Handlers (When Needed)
Some endpoints intentionally keep explicit handlers (for example multipart uploads such as /api/audio-transcribe).
When writing a manual handler, still mirror shared behavior:
getEffectiveOrigin()+isAllowedOrigin()+setCorsHeaders()
- method checks and
OPTIONShandling initLogger()+ status timing logsresolveRequestAuth()for authenticated routes
7. Logging
Use request-scoped logger utilities from _utils/_logging.ts:
import { initLogger } from "../_utils/_logging.js";
const { logger } = initLogger();
logger.request(req.method || "GET", req.url || "/api/example");
logger.info("Processing request", { user: "alice" });
logger.response(200, Date.now() - startTime);
8. Utility Modules
_utils/_validation.ts: username/room/message validation, profanity filtering, HTML escaping
_utils/_ssrf.ts:validatePublicUrl()andsafeFetchWithRedirects()for outbound URL safety- SSE endpoints set stream headers explicitly and emit structured events (no shared SSE module)
Constants
Use shared constants instead of hardcoding values:
- Runtime-neutral, frontend-shared constants live in
src/shared/(for examplesrc/shared/validation.tsfor password/message bounds,src/shared/redisKeys.tsfor canonical Redis key prefixes,src/shared/constants/realtime.tsfor realtime channels).
- Auth/session constants (token length, TTLs, grace period, rate-limit windows) live in
api/_utils/auth/_constants.ts. - Feature-specific constants stay in feature helper modules (for example
rooms/_helpers/_constants.ts,listen/_helpers/_constants.ts,songs/_constants.ts).
Best Practices
- Prefer
apiHandlerfor new JSON endpoints.
- Keep auth semantics consistent through
request-auth. - Apply rate limits to public and expensive routes.
- Validate all user-provided input before use.
- Keep response shapes stable and explicit.
- Use SSRF-safe fetch helpers for untrusted URLs.
- Add structured logs for request/response and key branch decisions.
- Keep endpoint docs updated whenever request/response contracts change.
Error Response Format
Most endpoints use:
{
"error": "Human-readable error message"
}
Some endpoints include extra fields (for example rate-limit metadata, validation details, or endpoint-specific diagnostics). Keep these additions additive and backward-compatible.