@riligar/agents-kit 1.11.0 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,268 @@
1
+ # Elysia Lifecycle Hooks
2
+
3
+ Lifecycle hooks allow you to intercept requests at different stages. They're the foundation for middleware, auth, logging, and error handling.
4
+
5
+ ## Lifecycle Order
6
+
7
+ ```
8
+ Request → onRequest → onParse → onBeforeHandle → Handler → onAfterHandle → onAfterResponse
9
+
10
+ onError (if error thrown)
11
+ ```
12
+
13
+ ## onRequest
14
+
15
+ First hook executed. Minimal context, best for early checks.
16
+
17
+ ```javascript
18
+ const app = new Elysia()
19
+ .onRequest(({ request }) => {
20
+ console.log(`${request.method} ${request.url}`)
21
+ })
22
+ ```
23
+
24
+ **Use cases:**
25
+ - Request logging
26
+ - Rate limiting
27
+ - IP blocking
28
+ - CORS headers
29
+
30
+ **Early return skips remaining lifecycle:**
31
+
32
+ ```javascript
33
+ .onRequest(({ request, set }) => {
34
+ if (isBlocked(request)) {
35
+ set.status = 403
36
+ return { error: 'Forbidden' }
37
+ }
38
+ })
39
+ ```
40
+
41
+ ## onBeforeHandle
42
+
43
+ Runs after validation, before handler. Has full context.
44
+
45
+ ```javascript
46
+ const app = new Elysia()
47
+ .onBeforeHandle(({ headers, set }) => {
48
+ if (!headers.authorization) {
49
+ set.status = 401
50
+ return { error: 'Unauthorized' }
51
+ }
52
+ })
53
+ ```
54
+
55
+ **Use cases:**
56
+ - Authentication checks
57
+ - Authorization
58
+ - Custom validation
59
+ - Request transformation
60
+
61
+ ## onAfterHandle
62
+
63
+ Runs after handler, can transform response.
64
+
65
+ ```javascript
66
+ const app = new Elysia()
67
+ .onAfterHandle(({ response, set }) => {
68
+ // Add header to all responses
69
+ set.headers['X-Powered-By'] = 'Elysia'
70
+
71
+ // Transform response
72
+ if (typeof response === 'object') {
73
+ return {
74
+ success: true,
75
+ data: response,
76
+ timestamp: Date.now()
77
+ }
78
+ }
79
+ })
80
+ ```
81
+
82
+ **Use cases:**
83
+ - Response transformation
84
+ - Adding headers
85
+ - Logging response
86
+
87
+ ## onError
88
+
89
+ Catches any error thrown during lifecycle.
90
+
91
+ ```javascript
92
+ const app = new Elysia()
93
+ .onError(({ code, error, set }) => {
94
+ console.error(error)
95
+
96
+ switch (code) {
97
+ case 'VALIDATION':
98
+ set.status = 422
99
+ return { error: 'Validation failed', details: error.all }
100
+
101
+ case 'NOT_FOUND':
102
+ set.status = 404
103
+ return { error: 'Not found' }
104
+
105
+ default:
106
+ set.status = 500
107
+ return { error: 'Internal server error' }
108
+ }
109
+ })
110
+ ```
111
+
112
+ **Error codes:**
113
+ - `VALIDATION` - Schema validation failed
114
+ - `NOT_FOUND` - Route not found
115
+ - `PARSE` - Body parsing failed
116
+ - `INTERNAL_SERVER_ERROR` - Unhandled error
117
+ - `UNKNOWN` - Unknown error type
118
+
119
+ ## onAfterResponse
120
+
121
+ Runs after response is sent. Cannot modify response.
122
+
123
+ ```javascript
124
+ const app = new Elysia()
125
+ .onAfterResponse(({ request, set }) => {
126
+ console.log(`Completed: ${request.method} ${request.url} - ${set.status}`)
127
+ })
128
+ ```
129
+
130
+ **Use cases:**
131
+ - Logging
132
+ - Analytics
133
+ - Cleanup
134
+
135
+ ## Local vs Interceptor Hooks
136
+
137
+ ### Local Hook (single route)
138
+
139
+ ```javascript
140
+ app.get('/users', () => getUsers(), {
141
+ beforeHandle: ({ headers, set }) => {
142
+ if (!headers.authorization) {
143
+ set.status = 401
144
+ return { error: 'Unauthorized' }
145
+ }
146
+ }
147
+ })
148
+ ```
149
+
150
+ ### Interceptor Hook (all subsequent routes)
151
+
152
+ ```javascript
153
+ const app = new Elysia()
154
+ .onBeforeHandle(() => console.log('Runs on all routes below'))
155
+ .get('/a', () => 'A') // Hook runs
156
+ .get('/b', () => 'B') // Hook runs
157
+ ```
158
+
159
+ ## Practical Examples
160
+
161
+ ### Authentication Middleware
162
+
163
+ ```javascript
164
+ const requireAuth = ({ headers, set }) => {
165
+ const token = headers.authorization?.replace('Bearer ', '')
166
+
167
+ if (!token) {
168
+ set.status = 401
169
+ return { error: 'Missing token' }
170
+ }
171
+
172
+ try {
173
+ const user = verifyToken(token)
174
+ // User available in handler
175
+ } catch {
176
+ set.status = 401
177
+ return { error: 'Invalid token' }
178
+ }
179
+ }
180
+
181
+ const app = new Elysia()
182
+ // Public routes
183
+ .get('/health', () => ({ status: 'ok' }))
184
+
185
+ // Protected routes
186
+ .guard({ beforeHandle: requireAuth }, app => app
187
+ .get('/profile', () => getProfile())
188
+ .put('/profile', ({ body }) => updateProfile(body))
189
+ )
190
+ ```
191
+
192
+ ### Request Logging
193
+
194
+ ```javascript
195
+ const logger = new Elysia({ name: 'logger' })
196
+ .onRequest(({ request }) => {
197
+ const start = Date.now()
198
+ request.startTime = start
199
+ })
200
+ .onAfterResponse(({ request, set }) => {
201
+ const duration = Date.now() - request.startTime
202
+ console.log(`${request.method} ${request.url} ${set.status} ${duration}ms`)
203
+ })
204
+
205
+ app.use(logger)
206
+ ```
207
+
208
+ ### Error Wrapper
209
+
210
+ ```javascript
211
+ const app = new Elysia()
212
+ .onError(({ code, error, path, set }) => {
213
+ // Log error
214
+ console.error({
215
+ path,
216
+ code,
217
+ message: error.message,
218
+ stack: error.stack
219
+ })
220
+
221
+ // Don't expose internal errors
222
+ if (code === 'INTERNAL_SERVER_ERROR') {
223
+ set.status = 500
224
+ return {
225
+ error: 'Something went wrong',
226
+ requestId: generateRequestId()
227
+ }
228
+ }
229
+
230
+ // Validation errors - expose details
231
+ if (code === 'VALIDATION') {
232
+ set.status = 422
233
+ return {
234
+ error: 'Validation failed',
235
+ details: error.all
236
+ }
237
+ }
238
+ })
239
+ ```
240
+
241
+ ### Rate Limiting
242
+
243
+ ```javascript
244
+ const rateLimits = new Map()
245
+
246
+ const rateLimit = (limit = 100, windowMs = 60000) => {
247
+ return new Elysia({ name: 'rate-limit' })
248
+ .onRequest(({ request, set }) => {
249
+ const ip = request.headers.get('x-forwarded-for') || 'unknown'
250
+ const now = Date.now()
251
+ const windowStart = now - windowMs
252
+
253
+ const requests = rateLimits.get(ip) || []
254
+ const recentRequests = requests.filter(t => t > windowStart)
255
+
256
+ if (recentRequests.length >= limit) {
257
+ set.status = 429
258
+ set.headers['Retry-After'] = Math.ceil(windowMs / 1000)
259
+ return { error: 'Too many requests' }
260
+ }
261
+
262
+ recentRequests.push(now)
263
+ rateLimits.set(ip, recentRequests)
264
+ })
265
+ }
266
+
267
+ app.use(rateLimit(100, 60000)) // 100 requests per minute
268
+ ```
@@ -0,0 +1,324 @@
1
+ # Elysia API Patterns
2
+
3
+ ## REST Resource Naming
4
+
5
+ ```
6
+ Rules:
7
+ ├── Use NOUNS, not verbs (resources, not actions)
8
+ ├── Use PLURAL forms (/users not /user)
9
+ ├── Use lowercase with hyphens (/user-profiles)
10
+ ├── Nest for relationships (/users/123/posts)
11
+ └── Keep shallow (max 3 levels deep)
12
+ ```
13
+
14
+ ### Examples
15
+
16
+ ```javascript
17
+ // Good
18
+ GET /users // List users
19
+ GET /users/:id // Get user
20
+ POST /users // Create user
21
+ PUT /users/:id // Replace user
22
+ PATCH /users/:id // Update user
23
+ DELETE /users/:id // Delete user
24
+
25
+ GET /users/:id/posts // User's posts
26
+ POST /users/:id/posts // Create post for user
27
+
28
+ // Bad
29
+ GET /getUsers
30
+ POST /createUser
31
+ GET /users/123/posts/456/comments/789/likes // Too deep
32
+ ```
33
+
34
+ ## HTTP Status Codes
35
+
36
+ | Situation | Code | When |
37
+ | --- | --- | --- |
38
+ | Success (read) | `200` | GET succeeded |
39
+ | Created | `201` | POST created resource |
40
+ | No content | `204` | DELETE succeeded |
41
+ | Bad request | `400` | Malformed request |
42
+ | Unauthorized | `401` | Missing/invalid auth |
43
+ | Forbidden | `403` | Valid auth, no permission |
44
+ | Not found | `404` | Resource doesn't exist |
45
+ | Conflict | `409` | State conflict (duplicate) |
46
+ | Validation error | `422` | Valid syntax, invalid data |
47
+ | Rate limited | `429` | Too many requests |
48
+ | Server error | `500` | Unhandled error |
49
+
50
+ ## Response Patterns
51
+
52
+ ### Success Response
53
+
54
+ ```javascript
55
+ // Single resource
56
+ app.get('/users/:id', ({ params, set }) => {
57
+ const user = getUserById(params.id)
58
+ if (!user) {
59
+ set.status = 404
60
+ return { error: 'User not found' }
61
+ }
62
+ return user
63
+ })
64
+
65
+ // List response
66
+ app.get('/users', () => {
67
+ return {
68
+ data: getUsers(),
69
+ total: getUserCount()
70
+ }
71
+ })
72
+
73
+ // Created response
74
+ app.post('/users', ({ body, set }) => {
75
+ const user = createUser(body)
76
+ set.status = 201
77
+ return user
78
+ })
79
+
80
+ // No content response
81
+ app.delete('/users/:id', ({ params, set }) => {
82
+ deleteUser(params.id)
83
+ set.status = 204
84
+ })
85
+ ```
86
+
87
+ ### Error Response
88
+
89
+ ```javascript
90
+ // Consistent error format
91
+ const errorResponse = (message, code, details = null) => ({
92
+ error: {
93
+ message,
94
+ code,
95
+ details
96
+ }
97
+ })
98
+
99
+ app.get('/users/:id', ({ params, set }) => {
100
+ const user = getUserById(params.id)
101
+ if (!user) {
102
+ set.status = 404
103
+ return errorResponse('User not found', 'USER_NOT_FOUND')
104
+ }
105
+ return user
106
+ })
107
+ ```
108
+
109
+ ## Pagination
110
+
111
+ ### Offset Pagination
112
+
113
+ ```javascript
114
+ import { Elysia, t } from 'elysia'
115
+
116
+ app.get('/users', ({ query }) => {
117
+ const { page = 1, limit = 10 } = query
118
+ const offset = (page - 1) * limit
119
+
120
+ const users = getUsers({ offset, limit })
121
+ const total = getUserCount()
122
+
123
+ return {
124
+ data: users,
125
+ pagination: {
126
+ page,
127
+ limit,
128
+ total,
129
+ totalPages: Math.ceil(total / limit)
130
+ }
131
+ }
132
+ }, {
133
+ query: t.Object({
134
+ page: t.Optional(t.Numeric({ default: 1, minimum: 1 })),
135
+ limit: t.Optional(t.Numeric({ default: 10, minimum: 1, maximum: 100 }))
136
+ })
137
+ })
138
+ ```
139
+
140
+ ### Cursor Pagination
141
+
142
+ ```javascript
143
+ app.get('/posts', ({ query }) => {
144
+ const { cursor, limit = 10 } = query
145
+
146
+ const posts = getPosts({ cursor, limit: limit + 1 })
147
+ const hasMore = posts.length > limit
148
+ const data = hasMore ? posts.slice(0, -1) : posts
149
+ const nextCursor = hasMore ? data[data.length - 1].id : null
150
+
151
+ return {
152
+ data,
153
+ pagination: {
154
+ nextCursor,
155
+ hasMore
156
+ }
157
+ }
158
+ }, {
159
+ query: t.Object({
160
+ cursor: t.Optional(t.String()),
161
+ limit: t.Optional(t.Numeric({ default: 10, maximum: 100 }))
162
+ })
163
+ })
164
+ ```
165
+
166
+ ## Filtering & Sorting
167
+
168
+ ```javascript
169
+ app.get('/products', ({ query }) => {
170
+ const { category, minPrice, maxPrice, sort = 'createdAt', order = 'desc' } = query
171
+
172
+ return getProducts({
173
+ filters: {
174
+ category,
175
+ price: { min: minPrice, max: maxPrice }
176
+ },
177
+ sort: { field: sort, order }
178
+ })
179
+ }, {
180
+ query: t.Object({
181
+ category: t.Optional(t.String()),
182
+ minPrice: t.Optional(t.Numeric()),
183
+ maxPrice: t.Optional(t.Numeric()),
184
+ sort: t.Optional(t.Union([
185
+ t.Literal('createdAt'),
186
+ t.Literal('price'),
187
+ t.Literal('name')
188
+ ])),
189
+ order: t.Optional(t.Union([
190
+ t.Literal('asc'),
191
+ t.Literal('desc')
192
+ ]))
193
+ })
194
+ })
195
+ ```
196
+
197
+ ## CRUD Resource Pattern
198
+
199
+ ```javascript
200
+ // routes/users.js
201
+ import { Elysia, t } from 'elysia'
202
+ import * as userService from '../services/user'
203
+
204
+ const UserSchema = t.Object({
205
+ name: t.String({ minLength: 1 }),
206
+ email: t.String({ format: 'email' })
207
+ })
208
+
209
+ const UserUpdateSchema = t.Partial(UserSchema)
210
+
211
+ export const userRoutes = new Elysia({ prefix: '/users' })
212
+ // List
213
+ .get('/', ({ query }) => {
214
+ return userService.findAll(query)
215
+ }, {
216
+ query: t.Object({
217
+ page: t.Optional(t.Numeric({ default: 1 })),
218
+ limit: t.Optional(t.Numeric({ default: 10 }))
219
+ })
220
+ })
221
+
222
+ // Get by ID
223
+ .get('/:id', ({ params, set }) => {
224
+ const user = userService.findById(params.id)
225
+ if (!user) {
226
+ set.status = 404
227
+ return { error: 'User not found' }
228
+ }
229
+ return user
230
+ }, {
231
+ params: t.Object({ id: t.Numeric() })
232
+ })
233
+
234
+ // Create
235
+ .post('/', ({ body, set }) => {
236
+ const user = userService.create(body)
237
+ set.status = 201
238
+ return user
239
+ }, {
240
+ body: UserSchema
241
+ })
242
+
243
+ // Update
244
+ .patch('/:id', ({ params, body, set }) => {
245
+ const user = userService.update(params.id, body)
246
+ if (!user) {
247
+ set.status = 404
248
+ return { error: 'User not found' }
249
+ }
250
+ return user
251
+ }, {
252
+ params: t.Object({ id: t.Numeric() }),
253
+ body: UserUpdateSchema
254
+ })
255
+
256
+ // Delete
257
+ .delete('/:id', ({ params, set }) => {
258
+ const deleted = userService.remove(params.id)
259
+ if (!deleted) {
260
+ set.status = 404
261
+ return { error: 'User not found' }
262
+ }
263
+ set.status = 204
264
+ }, {
265
+ params: t.Object({ id: t.Numeric() })
266
+ })
267
+ ```
268
+
269
+ ## API Versioning
270
+
271
+ ### URL Prefix (Recommended)
272
+
273
+ ```javascript
274
+ const v1Routes = new Elysia({ prefix: '/api/v1' })
275
+ .use(userRoutesV1)
276
+ .use(postRoutesV1)
277
+
278
+ const v2Routes = new Elysia({ prefix: '/api/v2' })
279
+ .use(userRoutesV2)
280
+
281
+ const app = new Elysia()
282
+ .use(v1Routes)
283
+ .use(v2Routes)
284
+ ```
285
+
286
+ ## Response Headers
287
+
288
+ ```javascript
289
+ // CORS
290
+ app.onRequest(({ set }) => {
291
+ set.headers['Access-Control-Allow-Origin'] = '*'
292
+ set.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, PATCH, DELETE'
293
+ })
294
+
295
+ // Cache Control
296
+ app.get('/static-data', ({ set }) => {
297
+ set.headers['Cache-Control'] = 'public, max-age=3600'
298
+ return getStaticData()
299
+ })
300
+
301
+ // Rate Limit Headers
302
+ app.onAfterHandle(({ set }) => {
303
+ set.headers['X-RateLimit-Limit'] = '100'
304
+ set.headers['X-RateLimit-Remaining'] = '99'
305
+ })
306
+ ```
307
+
308
+ ## Health Check
309
+
310
+ ```javascript
311
+ app.get('/health', () => ({
312
+ status: 'ok',
313
+ timestamp: new Date().toISOString(),
314
+ uptime: process.uptime()
315
+ }))
316
+
317
+ app.get('/health/ready', async () => {
318
+ const dbOk = await checkDatabase()
319
+ return {
320
+ status: dbOk ? 'ok' : 'error',
321
+ database: dbOk
322
+ }
323
+ })
324
+ ```