--- name: elysia-best-practices description: Use when writing, reviewing, or refactoring Elysia server code in this repository, including route handlers, schemas, response statuses, lifecycle hooks, plugins, OpenAPI docs, health checks, render API endpoints, and queue-facing API behavior. --- # Elysia Best Practices ## Overview Apply current Elysia guidance to this Bun Remotion rendering service while preserving its API contract: `/api/render` can enqueue async jobs, render synchronously, report BullMQ status, cancel jobs, and expose health checks. ## Workflow 1. Fetch current Elysia docs first with Context7. This repo currently uses `elysia` `1.4.27`, `@elysiajs/openapi` `1.4.14`, and `@elysiajs/swagger` `1.3.1`; compare docs against those packages before adopting examples that use newer `@elysia/openapi` imports. 2. Identify the layer being changed: - HTTP entrypoint: `server/index.ts` - Shared request schemas: `server/types/DocumentSchema.ts`, `server/types/CaptionStyleSchema.ts` - Queue behavior: `server/services/render_queue.ts` - Rendering and cleanup: `server/services/render_video.ts` - S3/webhooks/config: `server/services/s3.ts`, `server/services/webhook.ts`, `server/config.ts` 3. Keep Elysia routes thin. Validate and shape HTTP inputs at the route, then delegate rendering, queue, upload, and webhook work to services. 4. Verify with `bun run lint` and a focused smoke test through `bun run server`, `/api/health`, and the affected `/api/render` path. ## Project Map - Runtime: Bun with strict TypeScript and path aliases from `tsconfig.json`. - Server root: `new Elysia({ prefix: "/api" })` in `server/index.ts`. - Current endpoints: - `POST /api/render`: async queue when `callbackUrl` is present; synchronous render/upload fallback otherwise. - `GET /api/render/:renderId`: BullMQ job state and progress. - `DELETE /api/render/:renderId`: cancellation. - `GET /api/health`: liveness response. - Existing schemas use Elysia `t` and `Static`; reuse this style instead of introducing Zod, Valibot, or ad hoc runtime checks. ## Route Contracts - Define schemas for `body`, `params`, `query`, `headers`, and `response`. Elysia infers handler types from schemas, so avoid duplicate TypeScript interfaces unless they are exported from `Static`. - For route responses with multiple statuses, define `response` by status code and return `status(code, body)` from the handler. Prefer this for new code over mutating `set.status`; current docs call `set.status` legacy and it cannot validate response types as precisely. - Preserve existing API payload field names unless intentionally migrating a client contract: `renderId`, `status`, `progress_pct`, `output_path`, `callback_delivered`, and `error`. - Use literal unions for finite render states and style values. Keep captions and transcription schemas reusable in `server/types/`, not embedded inside handlers. - Validate URL-like inputs that leave the service boundary. For new fields such as callback or source URLs, prefer a schema-level constraint or a small service validator before queueing work. ## Status And Errors - Use the handler context `status()` function for expected outcomes: `return status(202, { renderId, status: "queued" })` and `return status(404, result)`. - Use `onError` for cross-cutting logging and sanitized error responses, and register it before routes it must affect. Do not leak S3 credentials, signed URLs, local output paths, full Remotion CLI logs, or Redis connection details. - Validation failures normally return 422. In production, Elysia omits detailed validation internals by default; keep that behavior unless debugging explicitly requires a temporary override. - When adding typed domain errors, register classes with `.error()` and narrow in `.onError()`. For one route only, use a local route `error` hook. ## Lifecycle And Plugins - Hook order matters. Interceptor hooks apply only to routes registered after them; place auth, tracing, error mapping, CORS, or request logging before the routes they should cover. - Elysia plugin lifecycles and schemas are encapsulated by default. Use `local`, `scoped`, or `global` intentionally when extracting route groups or shared guards, especially if a guard should affect parent `/api` routes. - Prefer `resolve` for per-request values that depend on validated data, such as a parsed render ID, normalized callback URL, or tenant/folder value. Use `derive` for request-derived context before validation only when validation is not needed first. - Use `decorate` for stable shared services or helpers, not request-specific mutable data. Avoid putting queue job state in Elysia `store`; BullMQ and Redis are the source of truth. - If route count grows, extract plugins by concern, for example `renderRoutes`, `healthRoutes`, and `openApiPlugin`, then mount them under the existing prefixed app. ## OpenAPI - If exposing docs, prefer one OpenAPI integration and make it match installed packages. This repo already has `@elysiajs/openapi` and `@elysiajs/swagger`; do not add `@elysia/openapi` without an intentional dependency migration. - Add `detail` metadata for public endpoints: tags, summary, description, and response examples where helpful. Keep docs accurate by defining runtime schemas for request and response shapes. - Treat render endpoints as operational API, not demo routes. Document async queue behavior, callback delivery, cancellation semantics, and sync fallback separately. ## Service Boundaries - Do not start extra BullMQ workers from route modules. The current server starts one worker at process boot; keep worker lifecycle explicit and graceful. - Keep cleanup in `finally` when route code creates local render outputs. Do not leave generated videos or props files behind after uploads or failures. - Avoid blocking request handlers with long synchronous work unless preserving the existing no-`callbackUrl` sync fallback. Prefer queueing for new expensive render operations. - Keep env requirements in `server/config.ts` and `.env.example` together. Never hardcode credentials, bucket names, Redis URLs, or public host assumptions in Elysia handlers. ## Validation - Always run `bun run lint` after TypeScript/Elysia changes. - For schema or status changes, smoke invalid requests and expected non-200 responses, not only the happy path. - For queue-facing changes, smoke `POST /api/render` with `callbackUrl`, `GET /api/render/:renderId`, and `DELETE /api/render/:renderId`. - For docs changes, verify the generated OpenAPI page/spec if the plugin is mounted. ## Source Anchors - Validation and response schemas: https://elysiajs.com/tutorial/getting-started/validation/ - Handler context and `status()`: https://elysiajs.com/essential/handler - Lifecycle and hook ordering: https://elysiajs.com/essential/life-cycle - Error handling: https://elysiajs.com/patterns/error-handling - Encapsulation, scopes, and guards: https://elysiajs.com/tutorial/getting-started/encapsulation/ - OpenAPI patterns: https://elysiajs.com/patterns/openapi