elysia

Elysia.js Patterns (v1.2+)

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "elysia" with this command: npx skills add epicenterhq/epicenter/epicenterhq-epicenter-elysia

Elysia.js Patterns (v1.2+)

The status() Helper (ALWAYS use this)

Never use set.status

  • return object. Always destructure status from the handler context and use it for all non-200 responses. This gives you:
  • Typesafe string literals with full IntelliSense (e.g. "Bad Request" instead of 400 )

  • Automatic response type inference per status code

  • Eden Treaty end-to-end type safety on error responses

Basic Usage

import { Elysia, t } from 'elysia';

new Elysia().post( '/chat', async ({ body, headers, status }) => { // ^^^^^^ destructure status from context

	if (!isValid(body.provider)) {
		// Use string literal for self-documenting, typesafe status codes
		return status('Bad Request', 'Unsupported provider');
	}

	if (!apiKey) {
		return status('Unauthorized', 'Missing API key');
	}

	return doWork(body);
},
{
	// Define response schemas per status code for full type safety
	response: {
		200: t.Any(),
		400: t.String(),
		401: t.String(),
	},
},

);

return status() vs throw status()

Both work. The framework handles either. The difference is purely control flow:

Pattern Behavior Use when

return status(...)

Normal return, continues to response pipeline You're at a natural return point (validation guards, end of handler)

throw status(...)

Short-circuits execution immediately You're deep in nested logic or inside a try/catch and want to bail out

This codebase convention: prefer return status(...) . It matches the existing early-return-on-error pattern used everywhere else (see error-handling skill). Reserve throw status(...) for catch blocks or deeply nested code where return would be awkward.

// GOOD: return for validation guards (matches codebase style) async ({ body, status }) => { if (!isValid(body.provider)) { return status('Bad Request', Unsupported provider: ${body.provider}); }

const apiKey = resolveApiKey(body.provider, headerApiKey);
if (!apiKey) {
	return status('Unauthorized', 'Missing API key');
}

// happy path
return doWork(body);

};

// GOOD: throw inside catch blocks async ({ body, status }) => { try { return await streamResponse(body); } catch (error) { if (isAbortError(error)) { throw status(499, 'Client closed request'); } throw status('Bad Gateway', Provider error: ${error.message}); } };

Type inference is identical for both

Both return status(...) and throw status(...) produce the same ElysiaCustomStatusResponse object. Elysia's type system infers response types from the response schema in route options, not from how you invoke status() . Eden Treaty type safety works equally with either approach.

Available String Status Codes (StatusMap)

Use these string literals instead of numeric codes for better readability:

String Literal Code Common Use

'Bad Request'

400 Validation failures, malformed input

'Unauthorized'

401 Missing/invalid auth credentials

'Forbidden'

403 Valid auth but insufficient permissions

'Not Found'

404 Resource doesn't exist

'Conflict'

409 State conflict (duplicate, already exists)

'Unprocessable Content'

422 Semantically invalid input

'Too Many Requests'

429 Rate limiting

'Internal Server Error'

500 Unexpected server failure

'Bad Gateway'

502 Upstream provider error

'Service Unavailable'

503 Temporary overload/maintenance

For non-standard codes (e.g. nginx's 499), use the numeric literal directly: status(499, 'Client closed request') .

Response Schemas for Eden Treaty Type Safety

Define response schemas per status code in route options. This is what makes Eden Treaty infer error types on the client:

new Elysia().post( '/chat', async ({ body, status }) => { if (!isValid(body.provider)) { return status('Bad Request', Unsupported provider: ${body.provider}); } return streamResult; }, { body: t.Object({ provider: t.String(), model: t.String(), }), response: { 200: t.Any(), // Success type 400: t.String(), // Bad Request body type 401: t.String(), // Unauthorized body type 502: t.String(), // Bad Gateway body type }, }, );

Eden Treaty then infers:

const { data, error } = await api.chat.post({ provider: 'openai', model: 'gpt-4', });

if (error) { // error.status is typed as 400 | 401 | 502 // error.value is typed per status code (string in this case) switch (error.status) { case 400: // error.value: string case 401: // error.value: string case 502: // error.value: string } }

Error Response Body: Strings vs Objects

Prefer plain strings as error bodies. The status code already communicates the error class. A descriptive string message is sufficient and keeps the API simple.

// GOOD: Plain string - status code provides the category return status('Bad Request', Unsupported provider: ${provider}); return status('Unauthorized', 'Missing API key: set x-provider-api-key header');

// AVOID: Wrapping in { error: "..." } object - redundant with status code set.status = 400; return { error: Unsupported provider: ${provider} };

If you need structured error bodies (multiple fields, error codes, validation details), define a TypeBox schema:

const ErrorBody = t.Object({ message: t.String(), code: t.Optional(t.String()), });

// In route options: response: { 400: ErrorBody, 401: ErrorBody, }

Plugin Composition

Elysia plugins are just functions that return Elysia instances. Use new Elysia() inside the plugin, not new Elysia({ prefix }) — let the consumer control mounting:

// GOOD: Plugin is prefix-agnostic export function createMyPlugin() { return new Elysia().post('/endpoint', async ({ body, status }) => { // ... }); }

// Consumer controls the prefix app.use(new Elysia({ prefix: '/api' }).use(createMyPlugin()));

Guards for Shared Auth

Use .guard() with beforeHandle for auth that applies to multiple routes:

const authed = new Elysia().guard({ async beforeHandle({ headers, status }) { const token = extractBearerToken(headers.authorization); if (!isValid(token)) { return status('Unauthorized', 'Invalid or missing token'); } }, });

// All routes under this guard require auth return authed .get('/protected', () => 'secret') .post('/admin', () => 'admin stuff');

Migration Checklist: set.status to status()

When updating existing handlers:

  • Replace set with status in the handler destructuring

  • Replace set.status = N; return { error: msg }; with return status('String Literal', msg);

  • In catch blocks, use throw status(...) instead of set.status = N; return { error: msg };

  • Add response schemas to route options for Eden Treaty type inference

  • Keep set in the destructuring ONLY if you still need set.headers for things like content-type

// BEFORE async ({ body, headers, set }) => { if (!valid) { set.status = 400; return { error: 'Bad input' }; } };

// AFTER async ({ body, headers, status }) => { if (!valid) { return status('Bad Request', 'Bad input'); } };

// AFTER (when you also need set.headers) async ({ body, headers, set, status }) => { if (!valid) { return status('Bad Request', 'Bad input'); } set.headers['content-type'] = 'application/octet-stream'; return binaryData; };

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

svelte

No summary provided by upstream source.

Repository SourceNeeds Review
General

documentation

No summary provided by upstream source.

Repository SourceNeeds Review
General

writing-voice

No summary provided by upstream source.

Repository SourceNeeds Review