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/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 # Switchable object storage (Vercel Blob / S3)
│ ├── 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
│ ├── _sse.ts # SSE stream helpers
│ ├── _logging.ts # Request-scoped logger
│ ├── _aiModels.ts # AI model registry and factory
│ ├── _aiPrompts.ts # Shared AI prompt templates
│ └── constants.ts # Shared constants
├── 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/ # Cloud backup + auto-sync 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. Runtime Declaration
export const runtime = "nodejs";
export const maxDuration = 30; // endpoint-dependent
2. apiHandler Wrapper (Primary Pattern)
import { apiHandler } from "../_utils/api-handler.js";
export default apiHandler(
{
methods: ["GET", "POST"],
auth: "optional", // "none" | "optional" | "required"
allowExpiredAuth: false, // optional
parseJsonBody: true, // optional
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
3. Endpoint Function Signature
When using apiHandler, handlers receive:
req,res
redislogger,startTimeoriginuser(nullor resolved auth user)body(nullor parsed JSON whenparseJsonBody: true)
4. Auth Boundaries via request-auth
resolveRequestAuth() enforces a shared policy:
- required auth endpoints need both headers:
Authorization: Bearer <token>X-Username: <username>
- partial credentials return
400 - invalid token/username pairs return
401 - optional-auth endpoints allow anonymous requests, but validate credentials if provided
For non-apiHandler routes (legacy/multipart), call resolveRequestAuth() directly to keep behavior aligned.
5. 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,
});
}
6. 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.).
7. 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
8. 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);
9. Utility Modules
_utils/_validation.ts: username/room/message validation, profanity filtering, HTML escaping
_utils/_ssrf.ts:validatePublicUrl()andsafeFetchWithRedirects()for outbound URL safety_utils/_sse.ts: SSE stream helper for chunked/line-based emissions_utils/constants.ts: shared prefixes, TTLs, rate-limit tiers, validation constraints
Constants
Use shared constants from _utils/constants.ts where possible instead of hardcoding:
REDIS_PREFIXES
TTLRATE_LIMIT_TIERSPASSWORDVALIDATIONTOKEN
Feature-specific constants should stay in feature helper modules (for example rooms/_helpers/_constants.ts, listen/_helpers/_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.