- Add principle 27: 'Language-Agnostic Auto-Trigger' to engineering-principles - Remove hardcoded English-only keyword lists from all 26 skill descriptions - Replace with concept-based detection: triggers from code context, project files, and file types regardless of spoken language - Update detect-project.sh hook output with language-agnostic message
5.1 KiB
5.1 KiB
name, description
| name | description |
|---|---|
| elysiajs | ElysiaJS (Bun) best practices — Eden Treaty, plugins, type-safe routes, Elysia validation, and middleware. Use when building ElysiaJS backend APIs. Triggers when working with this framework's files and patterns, not just explicit mentions." |
ElysiaJS Best Practices
Project Structure
src/
├── modules/ # Feature modules
│ ├── users/
│ │ ├── routes.ts # Elysia routes (thin)
│ │ ├── service.ts # Business logic
│ │ └── repository.ts # Data access
│ └── orders/
├── plugins/ # Custom Elysia plugins
├── lib/ # Shared utilities
├── db/ # Database schema, migrations
└── index.ts # App entry point
Route Definition (Type-Safe)
import { Elysia, t } from 'elysia';
import { userService } from './service';
const users = new Elysia({ prefix: '/users' })
.model({
'user.create': t.Object({
email: t.String({ format: 'email' }),
name: t.Optional(t.String({ minLength: 1 })),
}),
'user.response': t.Object({
id: t.String(),
email: t.String(),
name: t.Optional(t.String()),
}),
})
.get('/', async ({ query }) => {
const result = await userService.list(query);
return result;
}, {
query: t.Object({
page: t.Optional(t.Numeric({ minimum: 1 })),
limit: t.Optional(t.Numeric({ minimum: 1, maximum: 100 })),
}),
response: t.Array(t.Ref('user.response')),
})
.post('/', async ({ body }) => {
const user = await userService.create(body);
return user;
}, {
body: t.Ref('user.create'),
response: t.Ref('user.response'),
detail: { summary: 'Create user', tags: ['Users'] },
});
export { users };
Eden Treaty (Full-Stack Type Safety)
// Server (route definition inline above creates Eden types automatically)
// Client — automatically typed
import { treaty } from '@elysiajs/eden';
import type { App } from '../server';
const api = treaty<App>('http://localhost:3000');
// Fully typed — autocomplete for paths, params, response
const { data, error } = await api.users.index.get({ query: { page: 1, limit: 20 } });
// data is typed as UserResponse[]
Plugins Pattern
// Custom plugin — encapsulate cross-cutting concerns
import { Elysia } from 'elysia';
const authPlugin = (app: Elysia) =>
app
.decorate('auth', new AuthService())
.derive(({ headers, auth }) => {
const token = headers.authorization?.split(' ')[1];
const user = token ? auth.verify(token) : null;
return { user };
})
.onError(({ code, error }) => {
if (code === 'VALIDATION') return { error: error.message };
});
// Apply to app
const app = new Elysia()
.use(authPlugin)
.use(cors())
.use(swagger())
.group('/api/v1', (app) => app.use(users))
.listen(3000);
Validation
import { t } from 'elysia';
// Reusable models
const PaginationModel = t.Object({
page: t.Numeric({ minimum: 1, default: 1 }),
limit: t.Numeric({ minimum: 1, maximum: 100, default: 20 }),
});
const ErrorModel = t.Object({
error: t.String(),
details: t.Optional(t.Array(t.Object({
field: t.String(),
message: t.String(),
}))),
});
// Use `model()` to share across routes
const app = new Elysia()
.model({
pagination: PaginationModel,
error: ErrorModel,
});
Error Handling
import { Elysia, NotFoundError, ValidationError } from 'elysia';
const app = new Elysia()
.onError(({ code, error, set }) => {
switch (code) {
case 'NOT_FOUND':
set.status = 404;
return { error: 'Resource not found' };
case 'VALIDATION':
set.status = 422;
return { error: error.message };
default:
set.status = 500;
console.error(error);
return { error: 'Internal server error' };
}
});
Performance
- Elysia runs on Bun — Bun is fast. No need for extra micro-optimizations initially.
- Use
scoped: truefor per-request state isolation. - Static routes — use
staticPluginfor serving files. - WebSocket — built-in WS support, no extra lib needed.
Testing
import { describe, expect, it } from 'bun:test';
import { Elysia } from 'elysia';
import { userRoutes } from './routes';
const app = new Elysia().use(userRoutes);
describe('users', () => {
it('returns 422 for invalid email', async () => {
const res = await app
.handle(new Request('http://localhost/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: 'not-an-email' }),
}));
expect(res.status).toBe(422);
});
});
Anti-patterns
- ❌ Business logic in route handlers — extract to service layer
- ❌ No validation on inputs — every route must have a schema
- ❌ Mixing Elysia/Express patterns — Elysia is not Express
- ❌
anytypes — Elysia's superpower is type-safety - ❌ Global state in plugins — use decorator/derive for per-request state
- ❌ Using
t.Any()— defeats validation