@shohaghinfo/aerojs 0.1.0 → 0.1.2

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.
package/AGENTS.md CHANGED
@@ -1,709 +1,709 @@
1
- # AeroJS AI Agent Architecture & Coding Guidelines
2
-
3
- > **Notice for AI Coding Assistants (Gemini, Claude, Cursor, Windsurf, Copilot, ChatGPT):**
4
- > This file is your canonical reference manual for writing correct, idiomatic, high-performance TypeScript code for applications built on **AeroJS**. Always adhere strictly to the conventions, patterns, and APIs documented here.
5
-
6
- ---
7
-
8
- ## 1. Framework Philosophy & Core Concepts
9
-
10
- - **Framework Name**: **AeroJS** (imported as `from 'aerojs'`)
11
- - **Package Identity**: `@aerojs` or `aerojs`
12
- - **Architecture**: Modern, layered Full-Stack MVC & Service-Oriented Architecture.
13
- - **Runtime**: Node.js (ESM modules, `"type": "module"`).
14
- - **Core Standard**: Zero external runtime dependencies in core engine. High-throughput HTTP routing, built-in Active Record ORM, DI container, and flexible view/frontend drivers.
15
-
16
- ### Layered Architecture Flow
17
- ```
18
- Incoming HTTP Request
19
- │
20
- [Middleware] (CORS, Security Headers, CSRF, Rate Limiting, Auth)
21
- │
22
- [Router] (Matches Path & HTTP Verb)
23
- │
24
- [Validator] (Validates request schema / payload before handler)
25
- │
26
- [Controller] (Extracts request inputs, calls Service layer)
27
- │
28
- [Service Layer] (Business logic, transactions, external APIs)
29
- │
30
- [Active Record Model / QueryBuilder / Database]
31
- │
32
- [Response / View Engine / Inertia.js]
33
- ```
34
-
35
- ---
36
-
37
- ## 2. Directory Structure & Responsibilities
38
-
39
- | Path | Responsibility | Permitted Imports & Logic |
40
- | :--- | :--- | :--- |
41
- | `app/controllers/` | HTTP handling, status codes, delegating to services | Models, Services, Validators, `AeroContext` |
42
- | `app/services/` | Reusable business logic, multi-model transactions | Models, `DB`, External APIs, Jobs, Mail |
43
- | `app/models/` | Active Record entity definitions, relationships, hooks | `Model`, `Relation` from `aerojs` |
44
- | `app/validators/` | Request schemas (JSON Schema / VineJS) | Schema validation utilities |
45
- | `app/middleware/` | Request interception, authentication, logging | `AeroContext`, `NextFunction` from `aerojs` |
46
- | `app/jobs/` | Asynchronous background tasks | `Job` from `aerojs` |
47
- | `config/` | Application configuration modules | Reads `process.env` |
48
- | `database/migrations/` | Database table creation & schema evolution | `Schema`, `TableBlueprint`, `Migration` from `aerojs` |
49
- | `routes/` | Route definitions (`api.ts`, `web.ts`) | Controllers, Middlewares, Validators |
50
- | `public/` | Public static assets (CSS, JS, images, robots.txt) | Static files only |
51
- | `storage/` | Runtime logs, uploaded files, cache | Disk I/O |
52
- | `tests/` | In-process integration & unit tests | `createTestClient`, `vitest` |
53
-
54
- ---
55
-
56
- ## 3. Routing & Controllers
57
-
58
- ### 3.1 Defining Routes (`routes/api.ts` & `routes/web.ts`)
59
-
60
- Always group related routes and use **Controller Tuples** `[ControllerClass, 'methodName']`:
61
-
62
- ```typescript
63
- import type { Router } from 'aerojs';
64
- import { UserController } from '../app/controllers/UserController.js';
65
- import { authMiddleware } from '../app/middleware/AuthMiddleware.js';
66
- import { createUserSchema } from '../app/validators/UserValidator.js';
67
-
68
- export function registerApiRoutes(router: Router): void {
69
- router.group('/api/v1', (api) => {
70
- // Public routes
71
- api.get('/health', async (ctx) => {
72
- ctx.json({ status: 'ok', uptime: process.uptime() });
73
- });
74
-
75
- // Resource routes
76
- api.group('/users', (users) => {
77
- users.get('/', [UserController, 'index']);
78
- users.get('/:id', [UserController, 'show']);
79
- users.post('/', [UserController, 'store']).schema(createUserSchema);
80
- users.put('/:id', [UserController, 'update']).middleware(authMiddleware);
81
- users.delete('/:id', [UserController, 'destroy']).middleware(authMiddleware);
82
- });
83
- });
84
- }
85
- ```
86
-
87
- ### 3.2 Writing Controllers (`app/controllers/`)
88
-
89
- Controllers must remain thin. Never write SQL queries or heavy business logic directly inside controllers; call the **Service Layer**:
90
-
91
- ```typescript
92
- import type { AeroContext } from 'aerojs';
93
- import { UserService } from '../services/UserService.js';
94
-
95
- export class UserController {
96
- private userService = new UserService();
97
-
98
- public async index(ctx: AeroContext): Promise<void> {
99
- const page = parseInt((ctx.req.query.page as string) || '1', 10);
100
- const users = await this.userService.paginate(page);
101
- ctx.status(200).json({ success: true, data: users });
102
- }
103
-
104
- public async show(ctx: AeroContext): Promise<void> {
105
- const id = ctx.req.params.id;
106
- const user = await this.userService.findById(id);
107
- if (!user) {
108
- ctx.status(404).json({ error: 'User not found' });
109
- return;
110
- }
111
- ctx.status(200).json({ success: true, data: user });
112
- }
113
-
114
- public async store(ctx: AeroContext): Promise<void> {
115
- const payload = ctx.body as any;
116
- const newUser = await this.userService.create(payload);
117
- ctx.status(201).json({ success: true, data: newUser });
118
- }
119
- }
120
- ```
121
-
122
- ### 3.3 AeroContext Cheatsheet
123
-
124
- ```typescript
125
- // Reading Inputs
126
- const id = ctx.req.params.id; // Route parameters: /users/:id
127
- const search = ctx.req.query.q; // Query string: ?q=term
128
- const body = ctx.body; // Parsed JSON or UrlEncoded body
129
- const auth = ctx.req.get('authorization'); // Request headers
130
- const cookie = ctx.cookies.get('token'); // Read cookies
131
-
132
- // Sending Responses
133
- ctx.status(200).json({ key: 'val' }); // JSON response
134
- ctx.html('<h1>Hello World</h1>'); // HTML response
135
- ctx.text('Plain text message'); // Plain text
136
- ctx.redirect('/dashboard', 302); // Redirect
137
- ctx.cookies.set('token', 'xyz', { // Set cookie
138
- httpOnly: true,
139
- secure: true,
140
- maxAge: 3600,
141
- });
142
- ```
143
-
144
- ---
145
-
146
- ## 4. Database, Active Record ORM & Migrations
147
-
148
- AeroJS features a first-class, built-in ORM with Active Record models, fluent QueryBuilder, and schema migrations.
149
-
150
- ### 4.1 Active Record Models (`app/models/`)
151
-
152
- Define models by extending `Model`:
153
-
154
- ```typescript
155
- import { Model } from 'aerojs';
156
- import { Post } from './Post.js';
157
- import { Profile } from './Profile.js';
158
-
159
- export class User extends Model {
160
- public static override table = 'users';
161
- public static override primaryKey = 'id';
162
- public static override fillable = ['name', 'email', 'role', 'password'];
163
- public static override hidden = ['password'];
164
- public static override softDeletes = true; // Enables deleted_at handling
165
-
166
- // Relationships
167
- public profile() {
168
- return this.hasOne(Profile, 'user_id');
169
- }
170
-
171
- public posts() {
172
- return this.hasMany(Post, 'user_id');
173
- }
174
- }
175
- ```
176
-
177
- #### Model Query & Persistence Operations
178
-
179
- ```typescript
180
- // Finding Records
181
- const user = await User.find(1);
182
- const user = await User.findOrFail(1); // Throws NotFoundError if missing
183
- const admin = await User.where('role', 'admin').first();
184
- const allActive = await User.where('is_active', true).get();
185
-
186
- // Eager Loading Relationships
187
- const usersWithPosts = await User.with('profile', 'posts').get();
188
-
189
- // Creating Records
190
- const newUser = await User.create({
191
- name: 'Jane Doe',
192
- email: 'jane@example.com',
193
- role: 'developer',
194
- });
195
-
196
- // Updating Records
197
- user.name = 'Jane Smith';
198
- await user.save();
199
-
200
- // Deleting Records
201
- await user.delete(); // Soft deletes if softDeletes = true
202
- await user.restore(); // Restores soft-deleted record
203
- ```
204
-
205
- ### 4.2 Fluent QueryBuilder (`DB.table(...)`)
206
-
207
- When complex aggregate queries, custom joins, or bulk operations are needed:
208
-
209
- ```typescript
210
- import { DB } from 'aerojs';
211
-
212
- // Select with conditions, joins & pagination
213
- const results = await DB.table('orders')
214
- .select('orders.*', 'users.name as customer_name')
215
- .join('users', 'orders.user_id', 'users.id')
216
- .where('orders.status', '=', 'completed')
217
- .whereIn('orders.currency', ['USD', 'EUR'])
218
- .orderBy('orders.created_at', 'DESC')
219
- .paginate(1, 15); // Returns { data, total, page, perPage, lastPage }
220
-
221
- // Aggregates
222
- const totalRevenue = await DB.table('orders').sum('total_amount');
223
- const orderCount = await DB.table('orders').where('status', 'pending').count();
224
-
225
- // Direct Insert, Update, Delete
226
- await DB.table('logs').insert({ event: 'login', ip: '127.0.0.1' });
227
- await DB.table('users').where('id', 5).update({ status: 'active' });
228
- await DB.table('sessions').where('expires_at', '<', new Date()).delete();
229
-
230
- // Raw SQL & Transactions
231
- const rawRows = await DB.query('SELECT * FROM users WHERE email = ?', ['admin@dev.com']);
232
-
233
- await DB.transaction(async (trx) => {
234
- await trx.execute('UPDATE accounts SET balance = balance - ? WHERE id = ?', [100, 1]);
235
- await trx.execute('UPDATE accounts SET balance = balance + ? WHERE id = ?', [100, 2]);
236
- });
237
- ```
238
-
239
- ### 4.3 Database Migrations (`database/migrations/`)
240
-
241
- Migrations define schema changes using `TableBlueprint`:
242
-
243
- ```typescript
244
- import { Schema, type Migration, type TableBlueprint } from 'aerojs';
245
-
246
- export default class CreateUsersTable implements Migration {
247
- public name = '2026_09_30_000000_create_users_table';
248
-
249
- public async up(): Promise<void> {
250
- await Schema.create('users', (table: TableBlueprint) => {
251
- table.increments('id');
252
- table.string('name', 255).notNull();
253
- table.string('email', 191).notNull().unique();
254
- table.string('role').defaultTo('user');
255
- table.boolean('is_active').defaultTo(true);
256
- table.text('bio').nullable();
257
- table.timestamps(); // Creates created_at and updated_at
258
- });
259
- }
260
-
261
- public async down(): Promise<void> {
262
- await Schema.dropIfExists('users');
263
- }
264
- }
265
- ```
266
-
267
- #### Blueprint Column Methods
268
- - `table.increments('id')`
269
- - `table.string('name', 255)`
270
- - `table.integer('age')`
271
- - `table.boolean('is_verified')`
272
- - `table.text('content')`
273
- - `table.timestamp('published_at')`
274
- - `table.timestamps()`
275
- - `.nullable()`, `.notNull()`, `.unique()`, `.defaultTo(val)`
276
-
277
- ### 4.4 Database Adapters, Dialects & Connection Pooling
278
-
279
- AeroJS supports multiple relational databases via Knex connection pooling, as well as native zero-dependency in-memory execution.
280
-
281
- #### Environment Variables (`.env`):
282
- ```env
283
- # Database (MySQL / PostgreSQL / SQLite)
284
- DB_CONNECTION=mysql
285
- DB_HOST=127.0.0.1
286
- DB_PORT=3306
287
- DB_USER=root
288
- DB_PASSWORD=
289
- DB_DATABASE=aerojs_app
290
- DB_POOL_MIN=2
291
- DB_POOL_MAX=20
292
- ```
293
-
294
- #### How Dialects & Pooling Work in `config/database.ts`:
295
- ```typescript
296
- const poolMin = parseInt(process.env.DB_POOL_MIN || '2', 10);
297
- const poolMax = parseInt(process.env.DB_POOL_MAX || '20', 10);
298
-
299
- export const databaseConfig = {
300
- default: (process.env.DB_CONNECTION || 'mysql').toLowerCase(),
301
- pool: { min: poolMin, max: poolMax },
302
- connections: {
303
- mysql: { client: 'mysql2', ... },
304
- postgres: { client: 'pg', ... },
305
- sqlite: { client: 'better-sqlite3', ... },
306
- memory: { client: 'memory' },
307
- }
308
- };
309
- ```
310
-
311
- #### 1. Using MySQL (`client: 'mysql2'`)
312
- ```typescript
313
- import knex from 'knex';
314
- import { useKnex } from 'aerojs';
315
- import { databaseConfig } from './config/database.js';
316
-
317
- const db = knex({
318
- client: 'mysql2',
319
- connection: databaseConfig.connections.mysql,
320
- pool: databaseConfig.pool,
321
- });
322
- useKnex(db); // Seamlessly binds to all Models and DB.table()
323
- ```
324
-
325
- #### 2. Using PostgreSQL (`client: 'pg'`)
326
- Change `.env` to `DB_CONNECTION=postgres`, `DB_PORT=5432`, `DB_USER=postgres`:
327
- ```typescript
328
- import knex from 'knex';
329
- import { useKnex } from 'aerojs';
330
- import { databaseConfig } from './config/database.js';
331
-
332
- const db = knex({
333
- client: 'pg',
334
- connection: databaseConfig.connections.postgres,
335
- pool: databaseConfig.pool,
336
- });
337
- useKnex(db);
338
- ```
339
-
340
- #### 3. Using SQLite (`client: 'better-sqlite3'`)
341
- Change `.env` to `DB_CONNECTION=sqlite`, `DB_DATABASE=storage/database.sqlite`:
342
- ```typescript
343
- import knex from 'knex';
344
- import { useKnex } from 'aerojs';
345
- import { databaseConfig } from './config/database.js';
346
-
347
- const db = knex({
348
- client: 'better-sqlite3',
349
- connection: databaseConfig.connections.sqlite,
350
- useNullAsDefault: true,
351
- });
352
- useKnex(db);
353
- ```
354
-
355
- #### 4. Connecting Prisma or Drizzle
356
- ```typescript
357
- import { usePrisma, useDrizzle } from 'aerojs';
358
-
359
- // Prisma
360
- usePrisma(prismaClient);
361
-
362
- // Drizzle
363
- useDrizzle(drizzleDb);
364
- ```
365
-
366
- ---
367
-
368
- ## 5. Frontend & View Engines (Vue, React, Edge.js, EJS)
369
-
370
- AeroJS supports multiple presentation tiers:
371
-
372
- ### 5.1 Inertia.js (React & Vue 3)
373
-
374
- For modern Single Page Applications with server-side routing:
375
-
376
- 1. **Enable in `server.ts`**:
377
- ```typescript
378
- app.useInertia({
379
- rootView: 'default', // Or custom HTML root template with vite() tags
380
- version: '1.0',
381
- });
382
- ```
383
-
384
- 2. **Render in Controller**:
385
- ```typescript
386
- export class UserController {
387
- public async index(ctx: AeroContext): Promise<void> {
388
- const users = await User.all();
389
- await ctx.inertia.render('Users/Index', {
390
- users,
391
- title: 'User Management',
392
- });
393
- }
394
- }
395
- ```
396
-
397
- ### 5.2 Official Vite Integration (`vite.config.ts` for React & Vue 3)
398
-
399
- AeroJS integrates seamlessly with **Vite** for ultra-fast Hot Module Replacement (HMR) during development and optimized asset hashing in production.
400
-
401
- #### 1. Directory Structure:
402
- ```text
403
- my-aero-app/
404
- ├── resources/
405
- │ ├── js/
406
- │ │ └── app.ts # Vue 3 / React entry point
407
- │ └── css/
408
- │ └── app.css # Tailwind CSS / Styles
409
- ├── public/
410
- │ └── build/ # Generated production assets & manifest.json
411
- └── vite.config.ts # Vite configuration
412
- ```
413
-
414
- #### 2. `vite.config.ts` Configuration:
415
- ```typescript
416
- import { defineConfig } from 'vite';
417
- // For React: import react from '@vitejs/plugin-react';
418
- // For Vue 3: import vue from '@vitejs/plugin-vue';
419
-
420
- export default defineConfig({
421
- plugins: [
422
- // react(), // or vue()
423
- ],
424
- build: {
425
- outDir: 'public/build',
426
- manifest: true, // Generates manifest.json with hashed chunks
427
- rollupOptions: {
428
- input: 'resources/js/app.ts', // or resources/js/app.tsx
429
- },
430
- },
431
- server: {
432
- cors: true,
433
- port: 5173,
434
- strictPort: true,
435
- },
436
- });
437
- ```
438
-
439
- #### 3. Injecting Vite Assets with AeroJS `vite()` Helper:
440
- AeroJS provides a built-in `vite(entry)` helper that auto-detects development vs production:
441
- - In Development: Injects `<script type="module" src="http://localhost:5173/@vite/client">` + entry scripts.
442
- - In Production: Automatically reads `public/build/manifest.json` and injects hashed `<link rel="stylesheet">` and `<script>` tags.
443
-
444
- ```typescript
445
- import { vite } from 'aerojs';
446
-
447
- // In your root Inertia view or HTML layout:
448
- app.useInertia({
449
- rootView: `
450
- <!DOCTYPE html>
451
- <html lang="en">
452
- <head>
453
- <meta charset="UTF-8">
454
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
455
- ${vite('resources/js/app.ts')}
456
- </head>
457
- <body>
458
- @inertia
459
- </body>
460
- </html>
461
- `,
462
- });
463
- ```
464
-
465
- #### 4. NPM Scripts (`package.json`):
466
- ```json
467
- {
468
- "scripts": {
469
- "dev": "concurrently \"tsx watch server.ts\" \"vite\"",
470
- "build": "tsc && vite build",
471
- "start": "node dist/server.js"
472
- }
473
- }
474
- ```
475
-
476
- ### 5.3 Edge.js Template Engine (AdonisJS Style)
477
-
478
- ```typescript
479
- import { Edge } from 'edge.js';
480
- import { createEdgeDriver } from 'aerojs';
481
-
482
- const edge = new Edge();
483
- edge.mount(new URL('./views', import.meta.url));
484
- app.useViewEngine(createEdgeDriver(edge));
485
-
486
- // In Route / Controller:
487
- router.get('/dashboard', async (ctx) => {
488
- await ctx.view('dashboard', { user: ctx.state.user });
489
- });
490
- ```
491
-
492
- ### 5.3 EJS Template Engine
493
-
494
- ```typescript
495
- import ejs from 'ejs';
496
- import { createEjsDriver } from 'aerojs';
497
-
498
- app.useViewEngine(createEjsDriver(ejs));
499
-
500
- // In Route / Controller:
501
- router.get('/about', async (ctx) => {
502
- await ctx.view('pages/about.ejs', { title: 'About Us' });
503
- });
504
- ```
505
-
506
- ### 5.4 SSR Engine
507
-
508
- AeroJS includes a built-in `SSREngine` supporting asynchronous streaming and string rendering for server-rendered React or Vue components.
509
-
510
- ---
511
-
512
- ## 6. Request Validation (`app/validators/`)
513
-
514
- Always validate request payloads before they reach business logic:
515
-
516
- ```typescript
517
- export const createUserSchema = {
518
- body: {
519
- type: 'object',
520
- required: ['name', 'email'],
521
- properties: {
522
- name: { type: 'string', minLength: 2, maxLength: 100 },
523
- email: { type: 'string', format: 'email' },
524
- role: { type: 'string', enum: ['admin', 'developer', 'user'] },
525
- },
526
- },
527
- };
528
- ```
529
-
530
- Attach to routes using `.schema(createUserSchema)`. AeroJS will automatically reject invalid requests with status `422 Unprocessable Entity`.
531
-
532
- ---
533
-
534
- ## 7. Background Queue Jobs & Mail
535
-
536
- ### 7.1 Jobs (`app/jobs/`)
537
-
538
- ```typescript
539
- import { Job, Queue } from 'aerojs';
540
-
541
- export class SendWelcomeEmailJob extends Job {
542
- public static override queue = 'emails';
543
- public static override maxTries = 3;
544
-
545
- public async handle(): Promise<void> {
546
- const { email, name } = this.data;
547
- console.log(`Sending welcome email to ${name} (${email})`);
548
- }
549
- }
550
-
551
- // Dispatching a Job:
552
- await Queue.push(new SendWelcomeEmailJob({ email: 'user@aerojs.dev', name: 'User' }));
553
- ```
554
-
555
- ### 7.2 Mail Sending
556
-
557
- ```typescript
558
- import { Mail } from 'aerojs';
559
-
560
- await Mail.send({
561
- to: 'user@example.com',
562
- subject: 'Welcome to our platform',
563
- html: '<h1>Welcome!</h1><p>Your account is now ready.</p>',
564
- });
565
- ```
566
-
567
- ---
568
-
569
- ## 8. Testing Conventions (`tests/`)
570
-
571
- Use AeroJS `createTestClient` with Vitest for fast, in-memory end-to-end testing without opening real network sockets:
572
-
573
- ```typescript
574
- import { describe, it, expect } from 'vitest';
575
- import { createTestClient } from 'aerojs/testing';
576
- import app from '../server.js';
577
-
578
- describe('User API Tests', () => {
579
- const client = createTestClient(app);
580
-
581
- it('GET /api/users returns list of users', async () => {
582
- const res = await client.get('/api/users');
583
- expect(res.status).toBe(200);
584
- expect(res.json().success).toBe(true);
585
- expect(Array.isArray(res.json().data)).toBe(true);
586
- });
587
-
588
- it('POST /api/users validates payload', async () => {
589
- const res = await client.post('/api/users', {
590
- body: { name: 'A' }, // Missing email & name too short
591
- });
592
- expect(res.status).toBe(422);
593
- });
594
- });
595
- ```
596
-
597
- ### 8.2 Next.js Style Interactive Error Dashboard
598
-
599
- In development mode (`debug: true` or `NODE_ENV !== 'production'`):
600
- - **Browser Requests (`Accept: text/html`)**: AeroJS intercepts unhandled exceptions and renders a rich dark-mode Error Dashboard. It parses the stack trace, extracts the local source code file, and highlights the exact crashing line with an error indicator (`→`). It also provides interactive call stack inspection, request headers/parameters explorer, and system diagnostics.
601
- - **API Requests (`Accept: application/json`)**: AeroJS returns clean JSON with `{ error: { message, status, code, stack } }`.
602
-
603
- ---
604
-
605
- ## 9. Frontend & View Engines (Edge.js, EJS, React, Vue 3)
606
-
607
- AeroJS supports both **Server-Side Template Engines (MPA)** and **Modern Single-Page Applications (SPA)** via Inertia.js protocol.
608
-
609
- ### 9.1 Edge.js (AdonisJS Official Template Engine)
610
- - **Install**: `npm install edge.js`
611
- - **Location**: `views/edge/*.edge`
612
- - **Syntax**:
613
- ```edge
614
- @each(product in products)
615
- <div class="product-card">
616
- <h3>{{ product.name }}</h3>
617
- <span>${{ product.price }}</span>
618
- @if(product.stock > 10)
619
- <span class="stock-ok">In Stock ({{ product.stock }})</span>
620
- @else
621
- <span class="stock-low">Low Stock ({{ product.stock }})</span>
622
- @endif
623
- </div>
624
- @endeach
625
- ```
626
- - **Handler**:
627
- ```ts
628
- import { renderEdge } from '../app/views/engine.js';
629
- router.get('/views/edge', async (ctx: AeroContext) => {
630
- const products = await DB.table('products').get();
631
- const html = await renderEdge('edge/products', { products });
632
- ctx.html(html);
633
- });
634
- ```
635
-
636
- ### 9.2 EJS (Embedded JavaScript)
637
- - **Install**: `npm install ejs && npm install -D @types/ejs`
638
- - **Location**: `views/ejs/*.ejs`
639
- - **Syntax**:
640
- ```ejs
641
- <% products.forEach(function(product) { %>
642
- <div class="product-card">
643
- <h3><%= product.name %></h3>
644
- <span>$<%= product.price %></span>
645
- <% if (product.stock > 10) { %>
646
- <span class="stock-ok">In Stock</span>
647
- <% } else { %>
648
- <span class="stock-low">Low Stock</span>
649
- <% } %>
650
- </div>
651
- <% }); %>
652
- ```
653
- - **Handler**:
654
- ```ts
655
- import { renderEjs } from '../app/views/engine.js';
656
- router.get('/views/ejs', async (ctx: AeroContext) => {
657
- const products = await DB.table('products').get();
658
- const html = await renderEjs('ejs/products.ejs', { products });
659
- ctx.html(html);
660
- });
661
- ```
662
-
663
- ### 9.3 React 18 (Inertia.js SPA)
664
- - **Install**: `@inertiajs/react react react-dom`
665
- - **Location**: `resources/js/Pages/ProductsReact.tsx`
666
- - **Handler**:
667
- ```ts
668
- router.get('/views/react', async (ctx: AeroContext) => {
669
- const products = await DB.table('products').get();
670
- await ctx.inertia.render('ProductsReact', { products });
671
- });
672
- ```
673
- - **Protocol**:
674
- - Initial visit from browser: Returns HTML shell with `<div id="app" data-page='{"component":"ProductsReact","props":{...}}'></div>`.
675
- - AJAX visit with `X-Inertia: true`: Returns lightweight JSON props.
676
-
677
- ### 9.4 Vue 3 (Inertia.js SPA)
678
- - **Install**: `@inertiajs/vue3 vue`
679
- - **Location**: `resources/js/Pages/ProductsVue.vue`
680
- - **Handler**:
681
- ```ts
682
- router.get('/views/vue', async (ctx: AeroContext) => {
683
- const products = await DB.table('products').get();
684
- await ctx.inertia.render('ProductsVue', { products });
685
- });
686
- ```
687
-
688
- ---
689
-
690
- ## 10. AI Agent Implementation Checklist
691
-
692
- When asked to build or modify any feature in an AeroJS project:
693
-
694
- 1. **New Route**: Register in `routes/api.ts` or `routes/web.ts` using `router.group()` and controller tuples `[Controller, 'action']`.
695
- 2. **New Controller**: Place in `app/controllers/`. Keep it lean; forward data to `app/services/`.
696
- 3. **New Business Logic**: Place in `app/services/`.
697
- 4. **New Entity / Table**:
698
- - Create migration in `database/migrations/` using `Schema.create('table_name', (table) => ...)`.
699
- - Create model in `app/models/` extending `Model`.
700
- 5. **New Input Validation**: Create schema in `app/validators/` and attach via `.schema(...)`.
701
- 6. **Async Tasks / Emails**: Create job in `app/jobs/` extending `Job` and dispatch via `Queue.push()`.
702
- 7. **Frontend Views**:
703
- - Use Edge.js (`views/edge/`) or EJS (`views/ejs/`) for Server-Rendered HTML.
704
- - Use Inertia.js React (`resources/js/Pages/*.tsx`) or Vue 3 (`resources/js/Pages/*.vue`) for SPAs.
705
- 8. **Verification**: Always verify changes by running:
706
- ```bash
707
- npm run build
708
- npm test
709
- ```
1
+ # AeroJS AI Agent Architecture & Coding Guidelines
2
+
3
+ > **Notice for AI Coding Assistants (Gemini, Claude, Cursor, Windsurf, Copilot, ChatGPT):**
4
+ > This file is your canonical reference manual for writing correct, idiomatic, high-performance TypeScript code for applications built on **AeroJS**. Always adhere strictly to the conventions, patterns, and APIs documented here.
5
+
6
+ ---
7
+
8
+ ## 1. Framework Philosophy & Core Concepts
9
+
10
+ - **Framework Name**: **AeroJS** (imported as `from 'aerojs'`)
11
+ - **Package Identity**: `@aerojs` or `aerojs`
12
+ - **Architecture**: Modern, layered Full-Stack MVC & Service-Oriented Architecture.
13
+ - **Runtime**: Node.js (ESM modules, `"type": "module"`).
14
+ - **Core Standard**: Zero external runtime dependencies in core engine. High-throughput HTTP routing, built-in Active Record ORM, DI container, and flexible view/frontend drivers.
15
+
16
+ ### Layered Architecture Flow
17
+ ```
18
+ Incoming HTTP Request
19
+ │
20
+ [Middleware] (CORS, Security Headers, CSRF, Rate Limiting, Auth)
21
+ │
22
+ [Router] (Matches Path & HTTP Verb)
23
+ │
24
+ [Validator] (Validates request schema / payload before handler)
25
+ │
26
+ [Controller] (Extracts request inputs, calls Service layer)
27
+ │
28
+ [Service Layer] (Business logic, transactions, external APIs)
29
+ │
30
+ [Active Record Model / QueryBuilder / Database]
31
+ │
32
+ [Response / View Engine / Inertia.js]
33
+ ```
34
+
35
+ ---
36
+
37
+ ## 2. Directory Structure & Responsibilities
38
+
39
+ | Path | Responsibility | Permitted Imports & Logic |
40
+ | :--- | :--- | :--- |
41
+ | `app/controllers/` | HTTP handling, status codes, delegating to services | Models, Services, Validators, `AeroContext` |
42
+ | `app/services/` | Reusable business logic, multi-model transactions | Models, `DB`, External APIs, Jobs, Mail |
43
+ | `app/models/` | Active Record entity definitions, relationships, hooks | `Model`, `Relation` from `aerojs` |
44
+ | `app/validators/` | Request schemas (JSON Schema / VineJS) | Schema validation utilities |
45
+ | `app/middleware/` | Request interception, authentication, logging | `AeroContext`, `NextFunction` from `aerojs` |
46
+ | `app/jobs/` | Asynchronous background tasks | `Job` from `aerojs` |
47
+ | `config/` | Application configuration modules | Reads `process.env` |
48
+ | `database/migrations/` | Database table creation & schema evolution | `Schema`, `TableBlueprint`, `Migration` from `aerojs` |
49
+ | `routes/` | Route definitions (`api.ts`, `web.ts`) | Controllers, Middlewares, Validators |
50
+ | `public/` | Public static assets (CSS, JS, images, robots.txt) | Static files only |
51
+ | `storage/` | Runtime logs, uploaded files, cache | Disk I/O |
52
+ | `tests/` | In-process integration & unit tests | `createTestClient`, `vitest` |
53
+
54
+ ---
55
+
56
+ ## 3. Routing & Controllers
57
+
58
+ ### 3.1 Defining Routes (`routes/api.ts` & `routes/web.ts`)
59
+
60
+ Always group related routes and use **Controller Tuples** `[ControllerClass, 'methodName']`:
61
+
62
+ ```typescript
63
+ import type { Router } from 'aerojs';
64
+ import { UserController } from '../app/controllers/UserController.js';
65
+ import { authMiddleware } from '../app/middleware/AuthMiddleware.js';
66
+ import { createUserSchema } from '../app/validators/UserValidator.js';
67
+
68
+ export function registerApiRoutes(router: Router): void {
69
+ router.group('/api/v1', (api) => {
70
+ // Public routes
71
+ api.get('/health', async (ctx) => {
72
+ ctx.json({ status: 'ok', uptime: process.uptime() });
73
+ });
74
+
75
+ // Resource routes
76
+ api.group('/users', (users) => {
77
+ users.get('/', [UserController, 'index']);
78
+ users.get('/:id', [UserController, 'show']);
79
+ users.post('/', [UserController, 'store']).schema(createUserSchema);
80
+ users.put('/:id', [UserController, 'update']).middleware(authMiddleware);
81
+ users.delete('/:id', [UserController, 'destroy']).middleware(authMiddleware);
82
+ });
83
+ });
84
+ }
85
+ ```
86
+
87
+ ### 3.2 Writing Controllers (`app/controllers/`)
88
+
89
+ Controllers must remain thin. Never write SQL queries or heavy business logic directly inside controllers; call the **Service Layer**:
90
+
91
+ ```typescript
92
+ import type { AeroContext } from 'aerojs';
93
+ import { UserService } from '../services/UserService.js';
94
+
95
+ export class UserController {
96
+ private userService = new UserService();
97
+
98
+ public async index(ctx: AeroContext): Promise<void> {
99
+ const page = parseInt((ctx.req.query.page as string) || '1', 10);
100
+ const users = await this.userService.paginate(page);
101
+ ctx.status(200).json({ success: true, data: users });
102
+ }
103
+
104
+ public async show(ctx: AeroContext): Promise<void> {
105
+ const id = ctx.req.params.id;
106
+ const user = await this.userService.findById(id);
107
+ if (!user) {
108
+ ctx.status(404).json({ error: 'User not found' });
109
+ return;
110
+ }
111
+ ctx.status(200).json({ success: true, data: user });
112
+ }
113
+
114
+ public async store(ctx: AeroContext): Promise<void> {
115
+ const payload = ctx.body as any;
116
+ const newUser = await this.userService.create(payload);
117
+ ctx.status(201).json({ success: true, data: newUser });
118
+ }
119
+ }
120
+ ```
121
+
122
+ ### 3.3 AeroContext Cheatsheet
123
+
124
+ ```typescript
125
+ // Reading Inputs
126
+ const id = ctx.req.params.id; // Route parameters: /users/:id
127
+ const search = ctx.req.query.q; // Query string: ?q=term
128
+ const body = ctx.body; // Parsed JSON or UrlEncoded body
129
+ const auth = ctx.req.get('authorization'); // Request headers
130
+ const cookie = ctx.cookies.get('token'); // Read cookies
131
+
132
+ // Sending Responses
133
+ ctx.status(200).json({ key: 'val' }); // JSON response
134
+ ctx.html('<h1>Hello World</h1>'); // HTML response
135
+ ctx.text('Plain text message'); // Plain text
136
+ ctx.redirect('/dashboard', 302); // Redirect
137
+ ctx.cookies.set('token', 'xyz', { // Set cookie
138
+ httpOnly: true,
139
+ secure: true,
140
+ maxAge: 3600,
141
+ });
142
+ ```
143
+
144
+ ---
145
+
146
+ ## 4. Database, Active Record ORM & Migrations
147
+
148
+ AeroJS features a first-class, built-in ORM with Active Record models, fluent QueryBuilder, and schema migrations.
149
+
150
+ ### 4.1 Active Record Models (`app/models/`)
151
+
152
+ Define models by extending `Model`:
153
+
154
+ ```typescript
155
+ import { Model } from 'aerojs';
156
+ import { Post } from './Post.js';
157
+ import { Profile } from './Profile.js';
158
+
159
+ export class User extends Model {
160
+ public static override table = 'users';
161
+ public static override primaryKey = 'id';
162
+ public static override fillable = ['name', 'email', 'role', 'password'];
163
+ public static override hidden = ['password'];
164
+ public static override softDeletes = true; // Enables deleted_at handling
165
+
166
+ // Relationships
167
+ public profile() {
168
+ return this.hasOne(Profile, 'user_id');
169
+ }
170
+
171
+ public posts() {
172
+ return this.hasMany(Post, 'user_id');
173
+ }
174
+ }
175
+ ```
176
+
177
+ #### Model Query & Persistence Operations
178
+
179
+ ```typescript
180
+ // Finding Records
181
+ const user = await User.find(1);
182
+ const user = await User.findOrFail(1); // Throws NotFoundError if missing
183
+ const admin = await User.where('role', 'admin').first();
184
+ const allActive = await User.where('is_active', true).get();
185
+
186
+ // Eager Loading Relationships
187
+ const usersWithPosts = await User.with('profile', 'posts').get();
188
+
189
+ // Creating Records
190
+ const newUser = await User.create({
191
+ name: 'Jane Doe',
192
+ email: 'jane@example.com',
193
+ role: 'developer',
194
+ });
195
+
196
+ // Updating Records
197
+ user.name = 'Jane Smith';
198
+ await user.save();
199
+
200
+ // Deleting Records
201
+ await user.delete(); // Soft deletes if softDeletes = true
202
+ await user.restore(); // Restores soft-deleted record
203
+ ```
204
+
205
+ ### 4.2 Fluent QueryBuilder (`DB.table(...)`)
206
+
207
+ When complex aggregate queries, custom joins, or bulk operations are needed:
208
+
209
+ ```typescript
210
+ import { DB } from 'aerojs';
211
+
212
+ // Select with conditions, joins & pagination
213
+ const results = await DB.table('orders')
214
+ .select('orders.*', 'users.name as customer_name')
215
+ .join('users', 'orders.user_id', 'users.id')
216
+ .where('orders.status', '=', 'completed')
217
+ .whereIn('orders.currency', ['USD', 'EUR'])
218
+ .orderBy('orders.created_at', 'DESC')
219
+ .paginate(1, 15); // Returns { data, total, page, perPage, lastPage }
220
+
221
+ // Aggregates
222
+ const totalRevenue = await DB.table('orders').sum('total_amount');
223
+ const orderCount = await DB.table('orders').where('status', 'pending').count();
224
+
225
+ // Direct Insert, Update, Delete
226
+ await DB.table('logs').insert({ event: 'login', ip: '127.0.0.1' });
227
+ await DB.table('users').where('id', 5).update({ status: 'active' });
228
+ await DB.table('sessions').where('expires_at', '<', new Date()).delete();
229
+
230
+ // Raw SQL & Transactions
231
+ const rawRows = await DB.query('SELECT * FROM users WHERE email = ?', ['admin@dev.com']);
232
+
233
+ await DB.transaction(async (trx) => {
234
+ await trx.execute('UPDATE accounts SET balance = balance - ? WHERE id = ?', [100, 1]);
235
+ await trx.execute('UPDATE accounts SET balance = balance + ? WHERE id = ?', [100, 2]);
236
+ });
237
+ ```
238
+
239
+ ### 4.3 Database Migrations (`database/migrations/`)
240
+
241
+ Migrations define schema changes using `TableBlueprint`:
242
+
243
+ ```typescript
244
+ import { Schema, type Migration, type TableBlueprint } from 'aerojs';
245
+
246
+ export default class CreateUsersTable implements Migration {
247
+ public name = '2026_09_30_000000_create_users_table';
248
+
249
+ public async up(): Promise<void> {
250
+ await Schema.create('users', (table: TableBlueprint) => {
251
+ table.increments('id');
252
+ table.string('name', 255).notNull();
253
+ table.string('email', 191).notNull().unique();
254
+ table.string('role').defaultTo('user');
255
+ table.boolean('is_active').defaultTo(true);
256
+ table.text('bio').nullable();
257
+ table.timestamps(); // Creates created_at and updated_at
258
+ });
259
+ }
260
+
261
+ public async down(): Promise<void> {
262
+ await Schema.dropIfExists('users');
263
+ }
264
+ }
265
+ ```
266
+
267
+ #### Blueprint Column Methods
268
+ - `table.increments('id')`
269
+ - `table.string('name', 255)`
270
+ - `table.integer('age')`
271
+ - `table.boolean('is_verified')`
272
+ - `table.text('content')`
273
+ - `table.timestamp('published_at')`
274
+ - `table.timestamps()`
275
+ - `.nullable()`, `.notNull()`, `.unique()`, `.defaultTo(val)`
276
+
277
+ ### 4.4 Database Adapters, Dialects & Connection Pooling
278
+
279
+ AeroJS supports multiple relational databases via Knex connection pooling, as well as native zero-dependency in-memory execution.
280
+
281
+ #### Environment Variables (`.env`):
282
+ ```env
283
+ # Database (MySQL / PostgreSQL / SQLite)
284
+ DB_CONNECTION=mysql
285
+ DB_HOST=127.0.0.1
286
+ DB_PORT=3306
287
+ DB_USER=root
288
+ DB_PASSWORD=
289
+ DB_DATABASE=aerojs_app
290
+ DB_POOL_MIN=2
291
+ DB_POOL_MAX=20
292
+ ```
293
+
294
+ #### How Dialects & Pooling Work in `config/database.ts`:
295
+ ```typescript
296
+ const poolMin = parseInt(process.env.DB_POOL_MIN || '2', 10);
297
+ const poolMax = parseInt(process.env.DB_POOL_MAX || '20', 10);
298
+
299
+ export const databaseConfig = {
300
+ default: (process.env.DB_CONNECTION || 'mysql').toLowerCase(),
301
+ pool: { min: poolMin, max: poolMax },
302
+ connections: {
303
+ mysql: { client: 'mysql2', ... },
304
+ postgres: { client: 'pg', ... },
305
+ sqlite: { client: 'better-sqlite3', ... },
306
+ memory: { client: 'memory' },
307
+ }
308
+ };
309
+ ```
310
+
311
+ #### 1. Using MySQL (`client: 'mysql2'`)
312
+ ```typescript
313
+ import knex from 'knex';
314
+ import { useKnex } from 'aerojs';
315
+ import { databaseConfig } from './config/database.js';
316
+
317
+ const db = knex({
318
+ client: 'mysql2',
319
+ connection: databaseConfig.connections.mysql,
320
+ pool: databaseConfig.pool,
321
+ });
322
+ useKnex(db); // Seamlessly binds to all Models and DB.table()
323
+ ```
324
+
325
+ #### 2. Using PostgreSQL (`client: 'pg'`)
326
+ Change `.env` to `DB_CONNECTION=postgres`, `DB_PORT=5432`, `DB_USER=postgres`:
327
+ ```typescript
328
+ import knex from 'knex';
329
+ import { useKnex } from 'aerojs';
330
+ import { databaseConfig } from './config/database.js';
331
+
332
+ const db = knex({
333
+ client: 'pg',
334
+ connection: databaseConfig.connections.postgres,
335
+ pool: databaseConfig.pool,
336
+ });
337
+ useKnex(db);
338
+ ```
339
+
340
+ #### 3. Using SQLite (`client: 'better-sqlite3'`)
341
+ Change `.env` to `DB_CONNECTION=sqlite`, `DB_DATABASE=storage/database.sqlite`:
342
+ ```typescript
343
+ import knex from 'knex';
344
+ import { useKnex } from 'aerojs';
345
+ import { databaseConfig } from './config/database.js';
346
+
347
+ const db = knex({
348
+ client: 'better-sqlite3',
349
+ connection: databaseConfig.connections.sqlite,
350
+ useNullAsDefault: true,
351
+ });
352
+ useKnex(db);
353
+ ```
354
+
355
+ #### 4. Connecting Prisma or Drizzle
356
+ ```typescript
357
+ import { usePrisma, useDrizzle } from 'aerojs';
358
+
359
+ // Prisma
360
+ usePrisma(prismaClient);
361
+
362
+ // Drizzle
363
+ useDrizzle(drizzleDb);
364
+ ```
365
+
366
+ ---
367
+
368
+ ## 5. Frontend & View Engines (Vue, React, Edge.js, EJS)
369
+
370
+ AeroJS supports multiple presentation tiers:
371
+
372
+ ### 5.1 Inertia.js (React & Vue 3)
373
+
374
+ For modern Single Page Applications with server-side routing:
375
+
376
+ 1. **Enable in `server.ts`**:
377
+ ```typescript
378
+ app.useInertia({
379
+ rootView: 'default', // Or custom HTML root template with vite() tags
380
+ version: '1.0',
381
+ });
382
+ ```
383
+
384
+ 2. **Render in Controller**:
385
+ ```typescript
386
+ export class UserController {
387
+ public async index(ctx: AeroContext): Promise<void> {
388
+ const users = await User.all();
389
+ await ctx.inertia.render('Users/Index', {
390
+ users,
391
+ title: 'User Management',
392
+ });
393
+ }
394
+ }
395
+ ```
396
+
397
+ ### 5.2 Official Vite Integration (`vite.config.ts` for React & Vue 3)
398
+
399
+ AeroJS integrates seamlessly with **Vite** for ultra-fast Hot Module Replacement (HMR) during development and optimized asset hashing in production.
400
+
401
+ #### 1. Directory Structure:
402
+ ```text
403
+ my-aero-app/
404
+ ├── resources/
405
+ │ ├── js/
406
+ │ │ └── app.ts # Vue 3 / React entry point
407
+ │ └── css/
408
+ │ └── app.css # Tailwind CSS / Styles
409
+ ├── public/
410
+ │ └── build/ # Generated production assets & manifest.json
411
+ └── vite.config.ts # Vite configuration
412
+ ```
413
+
414
+ #### 2. `vite.config.ts` Configuration:
415
+ ```typescript
416
+ import { defineConfig } from 'vite';
417
+ // For React: import react from '@vitejs/plugin-react';
418
+ // For Vue 3: import vue from '@vitejs/plugin-vue';
419
+
420
+ export default defineConfig({
421
+ plugins: [
422
+ // react(), // or vue()
423
+ ],
424
+ build: {
425
+ outDir: 'public/build',
426
+ manifest: true, // Generates manifest.json with hashed chunks
427
+ rollupOptions: {
428
+ input: 'resources/js/app.ts', // or resources/js/app.tsx
429
+ },
430
+ },
431
+ server: {
432
+ cors: true,
433
+ port: 5173,
434
+ strictPort: true,
435
+ },
436
+ });
437
+ ```
438
+
439
+ #### 3. Injecting Vite Assets with AeroJS `vite()` Helper:
440
+ AeroJS provides a built-in `vite(entry)` helper that auto-detects development vs production:
441
+ - In Development: Injects `<script type="module" src="http://localhost:5173/@vite/client">` + entry scripts.
442
+ - In Production: Automatically reads `public/build/manifest.json` and injects hashed `<link rel="stylesheet">` and `<script>` tags.
443
+
444
+ ```typescript
445
+ import { vite } from 'aerojs';
446
+
447
+ // In your root Inertia view or HTML layout:
448
+ app.useInertia({
449
+ rootView: `
450
+ <!DOCTYPE html>
451
+ <html lang="en">
452
+ <head>
453
+ <meta charset="UTF-8">
454
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
455
+ ${vite('resources/js/app.ts')}
456
+ </head>
457
+ <body>
458
+ @inertia
459
+ </body>
460
+ </html>
461
+ `,
462
+ });
463
+ ```
464
+
465
+ #### 4. NPM Scripts (`package.json`):
466
+ ```json
467
+ {
468
+ "scripts": {
469
+ "dev": "concurrently \"tsx watch server.ts\" \"vite\"",
470
+ "build": "tsc && vite build",
471
+ "start": "node dist/server.js"
472
+ }
473
+ }
474
+ ```
475
+
476
+ ### 5.3 Edge.js Template Engine (AdonisJS Style)
477
+
478
+ ```typescript
479
+ import { Edge } from 'edge.js';
480
+ import { createEdgeDriver } from 'aerojs';
481
+
482
+ const edge = new Edge();
483
+ edge.mount(new URL('./views', import.meta.url));
484
+ app.useViewEngine(createEdgeDriver(edge));
485
+
486
+ // In Route / Controller:
487
+ router.get('/dashboard', async (ctx) => {
488
+ await ctx.view('dashboard', { user: ctx.state.user });
489
+ });
490
+ ```
491
+
492
+ ### 5.3 EJS Template Engine
493
+
494
+ ```typescript
495
+ import ejs from 'ejs';
496
+ import { createEjsDriver } from 'aerojs';
497
+
498
+ app.useViewEngine(createEjsDriver(ejs));
499
+
500
+ // In Route / Controller:
501
+ router.get('/about', async (ctx) => {
502
+ await ctx.view('pages/about.ejs', { title: 'About Us' });
503
+ });
504
+ ```
505
+
506
+ ### 5.4 SSR Engine
507
+
508
+ AeroJS includes a built-in `SSREngine` supporting asynchronous streaming and string rendering for server-rendered React or Vue components.
509
+
510
+ ---
511
+
512
+ ## 6. Request Validation (`app/validators/`)
513
+
514
+ Always validate request payloads before they reach business logic:
515
+
516
+ ```typescript
517
+ export const createUserSchema = {
518
+ body: {
519
+ type: 'object',
520
+ required: ['name', 'email'],
521
+ properties: {
522
+ name: { type: 'string', minLength: 2, maxLength: 100 },
523
+ email: { type: 'string', format: 'email' },
524
+ role: { type: 'string', enum: ['admin', 'developer', 'user'] },
525
+ },
526
+ },
527
+ };
528
+ ```
529
+
530
+ Attach to routes using `.schema(createUserSchema)`. AeroJS will automatically reject invalid requests with status `422 Unprocessable Entity`.
531
+
532
+ ---
533
+
534
+ ## 7. Background Queue Jobs & Mail
535
+
536
+ ### 7.1 Jobs (`app/jobs/`)
537
+
538
+ ```typescript
539
+ import { Job, Queue } from 'aerojs';
540
+
541
+ export class SendWelcomeEmailJob extends Job {
542
+ public static override queue = 'emails';
543
+ public static override maxTries = 3;
544
+
545
+ public async handle(): Promise<void> {
546
+ const { email, name } = this.data;
547
+ console.log(`Sending welcome email to ${name} (${email})`);
548
+ }
549
+ }
550
+
551
+ // Dispatching a Job:
552
+ await Queue.push(new SendWelcomeEmailJob({ email: 'user@aerojs.dev', name: 'User' }));
553
+ ```
554
+
555
+ ### 7.2 Mail Sending
556
+
557
+ ```typescript
558
+ import { Mail } from 'aerojs';
559
+
560
+ await Mail.send({
561
+ to: 'user@example.com',
562
+ subject: 'Welcome to our platform',
563
+ html: '<h1>Welcome!</h1><p>Your account is now ready.</p>',
564
+ });
565
+ ```
566
+
567
+ ---
568
+
569
+ ## 8. Testing Conventions (`tests/`)
570
+
571
+ Use AeroJS `createTestClient` with Vitest for fast, in-memory end-to-end testing without opening real network sockets:
572
+
573
+ ```typescript
574
+ import { describe, it, expect } from 'vitest';
575
+ import { createTestClient } from 'aerojs/testing';
576
+ import app from '../server.js';
577
+
578
+ describe('User API Tests', () => {
579
+ const client = createTestClient(app);
580
+
581
+ it('GET /api/users returns list of users', async () => {
582
+ const res = await client.get('/api/users');
583
+ expect(res.status).toBe(200);
584
+ expect(res.json().success).toBe(true);
585
+ expect(Array.isArray(res.json().data)).toBe(true);
586
+ });
587
+
588
+ it('POST /api/users validates payload', async () => {
589
+ const res = await client.post('/api/users', {
590
+ body: { name: 'A' }, // Missing email & name too short
591
+ });
592
+ expect(res.status).toBe(422);
593
+ });
594
+ });
595
+ ```
596
+
597
+ ### 8.2 Next.js Style Interactive Error Dashboard
598
+
599
+ In development mode (`debug: true` or `NODE_ENV !== 'production'`):
600
+ - **Browser Requests (`Accept: text/html`)**: AeroJS intercepts unhandled exceptions and renders a rich dark-mode Error Dashboard. It parses the stack trace, extracts the local source code file, and highlights the exact crashing line with an error indicator (`→`). It also provides interactive call stack inspection, request headers/parameters explorer, and system diagnostics.
601
+ - **API Requests (`Accept: application/json`)**: AeroJS returns clean JSON with `{ error: { message, status, code, stack } }`.
602
+
603
+ ---
604
+
605
+ ## 9. Frontend & View Engines (Edge.js, EJS, React, Vue 3)
606
+
607
+ AeroJS supports both **Server-Side Template Engines (MPA)** and **Modern Single-Page Applications (SPA)** via Inertia.js protocol.
608
+
609
+ ### 9.1 Edge.js (AdonisJS Official Template Engine)
610
+ - **Install**: `npm install edge.js`
611
+ - **Location**: `views/edge/*.edge`
612
+ - **Syntax**:
613
+ ```edge
614
+ @each(product in products)
615
+ <div class="product-card">
616
+ <h3>{{ product.name }}</h3>
617
+ <span>${{ product.price }}</span>
618
+ @if(product.stock > 10)
619
+ <span class="stock-ok">In Stock ({{ product.stock }})</span>
620
+ @else
621
+ <span class="stock-low">Low Stock ({{ product.stock }})</span>
622
+ @endif
623
+ </div>
624
+ @endeach
625
+ ```
626
+ - **Handler**:
627
+ ```ts
628
+ import { renderEdge } from '../app/views/engine.js';
629
+ router.get('/views/edge', async (ctx: AeroContext) => {
630
+ const products = await DB.table('products').get();
631
+ const html = await renderEdge('edge/products', { products });
632
+ ctx.html(html);
633
+ });
634
+ ```
635
+
636
+ ### 9.2 EJS (Embedded JavaScript)
637
+ - **Install**: `npm install ejs && npm install -D @types/ejs`
638
+ - **Location**: `views/ejs/*.ejs`
639
+ - **Syntax**:
640
+ ```ejs
641
+ <% products.forEach(function(product) { %>
642
+ <div class="product-card">
643
+ <h3><%= product.name %></h3>
644
+ <span>$<%= product.price %></span>
645
+ <% if (product.stock > 10) { %>
646
+ <span class="stock-ok">In Stock</span>
647
+ <% } else { %>
648
+ <span class="stock-low">Low Stock</span>
649
+ <% } %>
650
+ </div>
651
+ <% }); %>
652
+ ```
653
+ - **Handler**:
654
+ ```ts
655
+ import { renderEjs } from '../app/views/engine.js';
656
+ router.get('/views/ejs', async (ctx: AeroContext) => {
657
+ const products = await DB.table('products').get();
658
+ const html = await renderEjs('ejs/products.ejs', { products });
659
+ ctx.html(html);
660
+ });
661
+ ```
662
+
663
+ ### 9.3 React 18 (Inertia.js SPA)
664
+ - **Install**: `@inertiajs/react react react-dom`
665
+ - **Location**: `resources/js/Pages/ProductsReact.tsx`
666
+ - **Handler**:
667
+ ```ts
668
+ router.get('/views/react', async (ctx: AeroContext) => {
669
+ const products = await DB.table('products').get();
670
+ await ctx.inertia.render('ProductsReact', { products });
671
+ });
672
+ ```
673
+ - **Protocol**:
674
+ - Initial visit from browser: Returns HTML shell with `<div id="app" data-page='{"component":"ProductsReact","props":{...}}'></div>`.
675
+ - AJAX visit with `X-Inertia: true`: Returns lightweight JSON props.
676
+
677
+ ### 9.4 Vue 3 (Inertia.js SPA)
678
+ - **Install**: `@inertiajs/vue3 vue`
679
+ - **Location**: `resources/js/Pages/ProductsVue.vue`
680
+ - **Handler**:
681
+ ```ts
682
+ router.get('/views/vue', async (ctx: AeroContext) => {
683
+ const products = await DB.table('products').get();
684
+ await ctx.inertia.render('ProductsVue', { products });
685
+ });
686
+ ```
687
+
688
+ ---
689
+
690
+ ## 10. AI Agent Implementation Checklist
691
+
692
+ When asked to build or modify any feature in an AeroJS project:
693
+
694
+ 1. **New Route**: Register in `routes/api.ts` or `routes/web.ts` using `router.group()` and controller tuples `[Controller, 'action']`.
695
+ 2. **New Controller**: Place in `app/controllers/`. Keep it lean; forward data to `app/services/`.
696
+ 3. **New Business Logic**: Place in `app/services/`.
697
+ 4. **New Entity / Table**:
698
+ - Create migration in `database/migrations/` using `Schema.create('table_name', (table) => ...)`.
699
+ - Create model in `app/models/` extending `Model`.
700
+ 5. **New Input Validation**: Create schema in `app/validators/` and attach via `.schema(...)`.
701
+ 6. **Async Tasks / Emails**: Create job in `app/jobs/` extending `Job` and dispatch via `Queue.push()`.
702
+ 7. **Frontend Views**:
703
+ - Use Edge.js (`views/edge/`) or EJS (`views/ejs/`) for Server-Rendered HTML.
704
+ - Use Inertia.js React (`resources/js/Pages/*.tsx`) or Vue 3 (`resources/js/Pages/*.vue`) for SPAs.
705
+ 8. **Verification**: Always verify changes by running:
706
+ ```bash
707
+ npm run build
708
+ npm test
709
+ ```