@engjts/nexus 0.1.6 → 0.1.8

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.
Files changed (78) hide show
  1. package/BENCHMARK_REPORT.md +343 -0
  2. package/dist/advanced/playground/generatePlaygroundHTML.d.ts.map +1 -1
  3. package/dist/advanced/playground/generatePlaygroundHTML.js +107 -0
  4. package/dist/advanced/playground/generatePlaygroundHTML.js.map +1 -1
  5. package/dist/advanced/playground/playground.d.ts +19 -0
  6. package/dist/advanced/playground/playground.d.ts.map +1 -1
  7. package/dist/advanced/playground/playground.js +70 -0
  8. package/dist/advanced/playground/playground.js.map +1 -1
  9. package/dist/advanced/playground/types.d.ts +20 -0
  10. package/dist/advanced/playground/types.d.ts.map +1 -1
  11. package/dist/cli/templates/generators.d.ts.map +1 -1
  12. package/dist/cli/templates/generators.js +16 -13
  13. package/dist/cli/templates/generators.js.map +1 -1
  14. package/dist/cli/templates/index.js +25 -25
  15. package/dist/core/application.d.ts +14 -0
  16. package/dist/core/application.d.ts.map +1 -1
  17. package/dist/core/application.js +173 -71
  18. package/dist/core/application.js.map +1 -1
  19. package/dist/core/context-pool.d.ts +2 -13
  20. package/dist/core/context-pool.d.ts.map +1 -1
  21. package/dist/core/context-pool.js +7 -45
  22. package/dist/core/context-pool.js.map +1 -1
  23. package/dist/core/context.d.ts +108 -5
  24. package/dist/core/context.d.ts.map +1 -1
  25. package/dist/core/context.js +449 -53
  26. package/dist/core/context.js.map +1 -1
  27. package/dist/core/index.d.ts +1 -0
  28. package/dist/core/index.d.ts.map +1 -1
  29. package/dist/core/index.js +9 -1
  30. package/dist/core/index.js.map +1 -1
  31. package/dist/core/middleware.d.ts +6 -0
  32. package/dist/core/middleware.d.ts.map +1 -1
  33. package/dist/core/middleware.js +83 -84
  34. package/dist/core/middleware.js.map +1 -1
  35. package/dist/core/performance/fast-json.d.ts +149 -0
  36. package/dist/core/performance/fast-json.d.ts.map +1 -0
  37. package/dist/core/performance/fast-json.js +473 -0
  38. package/dist/core/performance/fast-json.js.map +1 -0
  39. package/dist/core/router/file-router.d.ts +20 -7
  40. package/dist/core/router/file-router.d.ts.map +1 -1
  41. package/dist/core/router/file-router.js +41 -13
  42. package/dist/core/router/file-router.js.map +1 -1
  43. package/dist/core/router/index.d.ts +6 -0
  44. package/dist/core/router/index.d.ts.map +1 -1
  45. package/dist/core/router/index.js +33 -6
  46. package/dist/core/router/index.js.map +1 -1
  47. package/dist/core/router/radix-tree.d.ts +4 -1
  48. package/dist/core/router/radix-tree.d.ts.map +1 -1
  49. package/dist/core/router/radix-tree.js +7 -3
  50. package/dist/core/router/radix-tree.js.map +1 -1
  51. package/dist/core/serializer.d.ts +251 -0
  52. package/dist/core/serializer.d.ts.map +1 -0
  53. package/dist/core/serializer.js +290 -0
  54. package/dist/core/serializer.js.map +1 -0
  55. package/dist/core/types.d.ts +39 -1
  56. package/dist/core/types.d.ts.map +1 -1
  57. package/dist/core/types.js.map +1 -1
  58. package/dist/index.d.ts +1 -0
  59. package/dist/index.d.ts.map +1 -1
  60. package/dist/index.js +12 -2
  61. package/dist/index.js.map +1 -1
  62. package/package.json +3 -1
  63. package/src/advanced/playground/generatePlaygroundHTML.ts +107 -0
  64. package/src/advanced/playground/playground.ts +225 -145
  65. package/src/advanced/playground/types.ts +29 -0
  66. package/src/cli/templates/generators.ts +16 -13
  67. package/src/cli/templates/index.ts +25 -25
  68. package/src/core/application.ts +202 -84
  69. package/src/core/context-pool.ts +8 -56
  70. package/src/core/context.ts +497 -53
  71. package/src/core/index.ts +14 -0
  72. package/src/core/middleware.ts +99 -89
  73. package/src/core/router/file-router.ts +41 -12
  74. package/src/core/router/index.ts +213 -180
  75. package/src/core/router/radix-tree.ts +20 -4
  76. package/src/core/serializer.ts +397 -0
  77. package/src/core/types.ts +43 -1
  78. package/src/index.ts +17 -0
@@ -9,115 +9,195 @@ import { RouteConfig, HTTPMethod, Context, Plugin, SchemaConfig, RouteMeta } fro
9
9
  import { PlaygroundConfig, StoredRoute } from './types';
10
10
 
11
11
 
12
+ /**
13
+ * API Playground Plugin
14
+ * Provides an interactive API explorer with authentication and development-mode security
15
+ *
16
+ * @param config - Configuration options for the playground
17
+ * @returns Plugin instance
18
+ *
19
+ * @example
20
+ * ```typescript
21
+ * app.use(playground({
22
+ * path: '/playground',
23
+ * developmentOnly: true,
24
+ * auth: {
25
+ * username: 'admin',
26
+ * password: 'secret123'
27
+ * }
28
+ * }));
29
+ * ```
30
+ */
12
31
  export function playground(config: PlaygroundConfig = {}): Plugin {
13
- const resolvedConfig: PlaygroundConfig = {
14
- path: '/playground',
15
- title: 'API Playground',
16
- theme: 'dark',
17
- enableHistory: true,
18
- maxHistory: 50,
19
- enableVariables: true,
20
- defaultHeaders: { 'Content-Type': 'application/json' },
21
- variables: { baseUrl: 'http://localhost:3000', token: '' },
22
- ...config
23
- };
24
-
25
- const routes: StoredRoute[] = [];
26
- let detectedBaseUrl = '';
27
- let appInstance: Application | null = null;
28
-
29
- return {
30
- name: 'playground',
31
- version: '1.0.0',
32
-
33
- install(app: Application) {
34
- appInstance = app;
35
- const originalRoute = app.route.bind(app);
36
- const originalGet = app.get.bind(app);
37
- const originalPost = app.post.bind(app);
38
- const originalPut = app.put.bind(app);
39
- const originalDelete = app.delete.bind(app);
40
- const originalPatch = app.patch.bind(app);
41
-
42
- app.route = function (routeConfig: RouteConfig) {
43
- routes.push({
44
- method: routeConfig.method,
45
- path: routeConfig.path,
46
- schema: routeConfig.schema,
47
- meta: routeConfig.meta
48
- });
49
- return originalRoute(routeConfig);
50
- };
51
-
52
- const wrapMethod = (method: HTTPMethod, original: Function) => {
53
- return function (pathOrRoute: string | any, handlerOrConfig?: any) {
54
- // Class-based routing
55
- if (typeof pathOrRoute === 'object' && 'pathName' in pathOrRoute) {
56
- const route = pathOrRoute;
57
- routes.push({
58
- method,
59
- path: route.pathName,
60
- schema: route.schema?.(),
61
- meta: route.meta?.()
62
- });
63
- return original(pathOrRoute);
64
- }
65
-
66
- const path = pathOrRoute;
67
- if (typeof handlerOrConfig !== 'function' && handlerOrConfig) {
68
- routes.push({ method, path, schema: handlerOrConfig.schema, meta: handlerOrConfig.meta });
69
- } else {
70
- routes.push({ method, path });
71
- }
72
- return original(path, handlerOrConfig);
73
- };
74
- };
75
-
76
- app.get = wrapMethod('GET', originalGet);
77
- app.post = wrapMethod('POST', originalPost);
78
- app.put = wrapMethod('PUT', originalPut);
79
- app.delete = wrapMethod('DELETE', originalDelete);
80
- app.patch = wrapMethod('PATCH', originalPatch);
81
-
82
- const basePath = resolvedConfig.path!;
83
-
84
- originalGet(basePath, async (ctx: any) => {
85
- if (!detectedBaseUrl && ctx.raw?.req) {
86
- const req = ctx.raw.req;
87
- const protocol = req.headers['x-forwarded-proto'] || 'http';
88
- const host = req.headers.host || 'localhost:3000';
89
- detectedBaseUrl = `${protocol}://${host}`;
90
- }
91
- return {
92
- statusCode: 200,
93
- headers: { 'Content-Type': 'text/html; charset=utf-8' },
94
- body: generatePlaygroundHTML(resolvedConfig, detectedBaseUrl)
95
- };
96
- });
32
+ const resolvedConfig: PlaygroundConfig = {
33
+ path: '/playground',
34
+ title: 'API Playground',
35
+ theme: 'dark',
36
+ enableHistory: true,
37
+ maxHistory: 50,
38
+ enableVariables: true,
39
+ developmentOnly: true,
40
+ defaultHeaders: { 'Content-Type': 'application/json' },
41
+ variables: { baseUrl: 'http://localhost:3000', token: '' },
42
+ ...config
43
+ };
44
+
45
+ const routes: StoredRoute[] = [];
46
+ let detectedBaseUrl = '';
47
+ let appInstance: Application | null = null;
48
+
49
+ return {
50
+ name: 'playground',
51
+ version: '1.0.0',
97
52
 
98
- originalGet(basePath + '/api/routes', async (ctx: Context) => {
99
- // Combine intercepted routes with routes from router (for file-based routing)
100
- const allRoutes = getAllRoutes(routes, appInstance, basePath);
101
-
102
- const routesData = allRoutes.map(r => ({
103
- method: r.method,
104
- path: r.path,
105
- summary: r.meta?.summary || generateSummary(r.method as HTTPMethod, r.path),
106
- description: r.meta?.description,
107
- tags: r.meta?.tags || [getTagFromPath(r.path)],
108
- deprecated: r.meta?.deprecated,
109
- responses: r.meta?.responses,
110
- example: r.meta?.example,
111
- schema: {
112
- body: r.schema?.body ? zodToExample(r.schema.body) : null,
113
- query: r.schema?.query ? zodToParams(r.schema.query) : null,
114
- params: extractPathParams(r.path)
115
- }
116
- }));
117
- return ctx.json(routesData);
53
+ install(app: Application) {
54
+ // Check if playground should be disabled in production
55
+ if (resolvedConfig.developmentOnly && process.env.NODE_ENV === 'production') {
56
+ console.warn('⚠️ API Playground is disabled in production mode');
57
+ return;
58
+ }
59
+
60
+ appInstance = app;
61
+ const originalRoute = app.route.bind(app);
62
+ const originalGet = app.get.bind(app);
63
+ const originalPost = app.post.bind(app);
64
+ const originalPut = app.put.bind(app);
65
+ const originalDelete = app.delete.bind(app);
66
+ const originalPatch = app.patch.bind(app);
67
+
68
+ app.route = function (routeConfig: RouteConfig) {
69
+ routes.push({
70
+ method: routeConfig.method,
71
+ path: routeConfig.path,
72
+ schema: routeConfig.schema,
73
+ meta: routeConfig.meta
74
+ });
75
+ return originalRoute(routeConfig);
76
+ };
77
+
78
+ const wrapMethod = (method: HTTPMethod, original: Function) => {
79
+ return function (pathOrRoute: string | any, handlerOrConfig?: any) {
80
+ // Class-based routing
81
+ if (typeof pathOrRoute === 'object' && 'pathName' in pathOrRoute) {
82
+ const route = pathOrRoute;
83
+ routes.push({
84
+ method,
85
+ path: route.pathName,
86
+ schema: route.schema?.(),
87
+ meta: route.meta?.()
118
88
  });
89
+ return original(pathOrRoute);
90
+ }
91
+
92
+ const path = pathOrRoute;
93
+ if (typeof handlerOrConfig !== 'function' && handlerOrConfig) {
94
+ routes.push({ method, path, schema: handlerOrConfig.schema, meta: handlerOrConfig.meta });
95
+ } else {
96
+ routes.push({ method, path });
97
+ }
98
+ return original(path, handlerOrConfig);
99
+ };
100
+ };
101
+
102
+ app.get = wrapMethod('GET', originalGet);
103
+ app.post = wrapMethod('POST', originalPost);
104
+ app.put = wrapMethod('PUT', originalPut);
105
+ app.delete = wrapMethod('DELETE', originalDelete);
106
+ app.patch = wrapMethod('PATCH', originalPatch);
107
+
108
+ const basePath = resolvedConfig.path!;
109
+
110
+ /**
111
+ * Authentication middleware for playground routes
112
+ * Implements HTTP Basic Authentication when auth config is provided
113
+ */
114
+ const authMiddleware = (ctx: any) => {
115
+ if (!resolvedConfig.auth) {
116
+ return true; // No auth required
117
+ }
118
+
119
+ const authHeader = ctx.raw?.req?.headers?.authorization || ctx.headers?.get?.('authorization');
120
+
121
+ if (!authHeader || !authHeader.startsWith('Basic ')) {
122
+ return false;
123
+ }
124
+
125
+ try {
126
+ const base64Credentials = authHeader.substring(6);
127
+ const credentials = Buffer.from(base64Credentials, 'base64').toString('utf-8');
128
+ const [username, password] = credentials.split(':');
129
+
130
+ return (
131
+ username === resolvedConfig.auth.username &&
132
+ password === resolvedConfig.auth.password
133
+ );
134
+ } catch (error) {
135
+ return false;
119
136
  }
120
- };
137
+ };
138
+
139
+ /**
140
+ * Returns 401 Unauthorized response with WWW-Authenticate header
141
+ * This triggers the browser's native authentication dialog
142
+ */
143
+ const unauthorizedResponse = () => {
144
+ return {
145
+ statusCode: 401,
146
+ headers: {
147
+ 'WWW-Authenticate': 'Basic realm="API Playground"',
148
+ 'Content-Type': 'text/plain'
149
+ },
150
+ body: 'Unauthorized'
151
+ };
152
+ };
153
+
154
+ originalGet(basePath, async (ctx: any) => {
155
+ // Check authentication
156
+ if (!authMiddleware(ctx)) {
157
+ return unauthorizedResponse();
158
+ }
159
+
160
+ if (!detectedBaseUrl && ctx.raw?.req) {
161
+ const req = ctx.raw.req;
162
+ const protocol = req.headers['x-forwarded-proto'] || 'http';
163
+ const host = req.headers.host || 'localhost:3000';
164
+ detectedBaseUrl = `${protocol}://${host}`;
165
+ }
166
+ return {
167
+ statusCode: 200,
168
+ headers: { 'Content-Type': 'text/html; charset=utf-8' },
169
+ body: generatePlaygroundHTML(resolvedConfig, detectedBaseUrl)
170
+ };
171
+ });
172
+
173
+ originalGet(basePath + '/api/routes', async (ctx: Context) => {
174
+ // Check authentication
175
+ if (!authMiddleware(ctx)) {
176
+ return unauthorizedResponse();
177
+ }
178
+
179
+ // Combine intercepted routes with routes from router (for file-based routing)
180
+ const allRoutes = getAllRoutes(routes, appInstance, basePath);
181
+
182
+ const routesData = allRoutes.map(r => ({
183
+ method: r.method,
184
+ path: r.path,
185
+ summary: r.meta?.summary || generateSummary(r.method as HTTPMethod, r.path),
186
+ description: r.meta?.description,
187
+ tags: r.meta?.tags || [getTagFromPath(r.path)],
188
+ deprecated: r.meta?.deprecated,
189
+ responses: r.meta?.responses,
190
+ example: r.meta?.example,
191
+ schema: {
192
+ body: r.schema?.body ? zodToExample(r.schema.body) : null,
193
+ query: r.schema?.query ? zodToParams(r.schema.query) : null,
194
+ params: extractPathParams(r.path)
195
+ }
196
+ }));
197
+ return ctx.json(routesData);
198
+ });
199
+ }
200
+ };
121
201
  }
122
202
 
123
203
  /**
@@ -125,46 +205,46 @@ export function playground(config: PlaygroundConfig = {}): Plugin {
125
205
  * This ensures file-based routes are also included
126
206
  */
127
207
  function getAllRoutes(
128
- interceptedRoutes: StoredRoute[],
129
- app: Application | null,
130
- basePath: string
208
+ interceptedRoutes: StoredRoute[],
209
+ app: Application | null,
210
+ basePath: string
131
211
  ): StoredRoute[] {
132
- // Create a Set of already tracked route keys (method + path)
133
- const trackedKeys = new Set(
134
- interceptedRoutes.map(r => `${r.method}:${r.path}`)
135
- );
136
-
137
- // Start with intercepted routes (filtered)
138
- const allRoutes: StoredRoute[] = interceptedRoutes.filter(
139
- r => !r.path.startsWith(basePath)
140
- );
141
-
142
- // Get routes from router (includes file-based routes)
143
- if (app) {
144
- const routerRoutes = app.getRoutes() as Array<{
145
- method: string;
146
- path: string;
147
- schema?: SchemaConfig;
148
- meta?: RouteMeta;
149
- }>;
150
-
151
- for (const route of routerRoutes) {
152
- const key = `${route.method}:${route.path}`;
153
-
154
- // Skip if already tracked or is playground route
155
- if (trackedKeys.has(key) || route.path.startsWith(basePath)) {
156
- continue;
157
- }
158
-
159
- allRoutes.push({
160
- method: route.method as HTTPMethod,
161
- path: route.path,
162
- schema: route.schema,
163
- meta: route.meta
164
- });
165
- }
212
+ // Create a Set of already tracked route keys (method + path)
213
+ const trackedKeys = new Set(
214
+ interceptedRoutes.map(r => `${r.method}:${r.path}`)
215
+ );
216
+
217
+ // Start with intercepted routes (filtered)
218
+ const allRoutes: StoredRoute[] = interceptedRoutes.filter(
219
+ r => !r.path.startsWith(basePath)
220
+ );
221
+
222
+ // Get routes from router (includes file-based routes)
223
+ if (app) {
224
+ const routerRoutes = app.getRoutes() as Array<{
225
+ method: string;
226
+ path: string;
227
+ schema?: SchemaConfig;
228
+ meta?: RouteMeta;
229
+ }>;
230
+
231
+ for (const route of routerRoutes) {
232
+ const key = `${route.method}:${route.path}`;
233
+
234
+ // Skip if already tracked or is playground route
235
+ if (trackedKeys.has(key) || route.path.startsWith(basePath)) {
236
+ continue;
237
+ }
238
+
239
+ allRoutes.push({
240
+ method: route.method as HTTPMethod,
241
+ path: route.path,
242
+ schema: route.schema,
243
+ meta: route.meta
244
+ });
166
245
  }
167
-
168
- // Sort routes by path for consistent ordering
169
- return allRoutes.sort((a, b) => a.path.localeCompare(b.path));
246
+ }
247
+
248
+ // Sort routes by path for consistent ordering
249
+ return allRoutes.sort((a, b) => a.path.localeCompare(b.path));
170
250
  }
@@ -1,14 +1,43 @@
1
1
  import { HTTPMethod, RouteMeta, SchemaConfig } from "../../core/types";
2
2
 
3
+ /**
4
+ * Configuration options for the API Playground
5
+ */
3
6
  export interface PlaygroundConfig {
7
+ /** URL path where the playground will be accessible (default: '/playground') */
4
8
  path?: string;
9
+
10
+ /** Title displayed in the playground UI (default: 'API Playground') */
5
11
  title?: string;
12
+
13
+ /** Color theme for the playground interface (default: 'dark') */
6
14
  theme?: 'dark' | 'light';
15
+
16
+ /** Default HTTP headers to include in API requests */
7
17
  defaultHeaders?: Record<string, string>;
18
+
19
+ /** Enable/disable request history tracking (default: true) */
8
20
  enableHistory?: boolean;
21
+
22
+ /** Maximum number of requests to keep in history (default: 50) */
9
23
  maxHistory?: number;
24
+
25
+ /** Enable/disable variable substitution in requests (default: true) */
10
26
  enableVariables?: boolean;
27
+
28
+ /** Predefined variables for use in requests (e.g., {{baseUrl}}, {{token}}) */
11
29
  variables?: Record<string, string>;
30
+
31
+ /** Enable only in development mode (default: true) */
32
+ developmentOnly?: boolean;
33
+
34
+ /** Basic authentication configuration to protect playground access */
35
+ auth?: {
36
+ /** Username for basic authentication */
37
+ username: string;
38
+ /** Password for basic authentication */
39
+ password: string;
40
+ };
12
41
  }
13
42
 
14
43
 
@@ -149,47 +149,50 @@ export const ${middlewareName}Middleware: Middleware = async (ctx: Context, next
149
149
  @Controller('/${baseName}')
150
150
  export class ${className} {
151
151
  @Get('/')
152
- async getAll(ctx: Context) {
153
- return ctx.json({
152
+ async getAll() {
153
+ return {
154
154
  message: 'Get all ${baseName}',
155
155
  data: [],
156
- });
156
+ };
157
157
  }
158
158
 
159
159
  @Get('/:id')
160
160
  async getById(ctx: Context) {
161
161
  const { id } = ctx.params;
162
- return ctx.json({
162
+ return {
163
163
  message: \`Get ${baseName} by id: \${id}\`,
164
164
  data: null,
165
- });
165
+ };
166
166
  }
167
167
 
168
168
  @Post('/')
169
169
  async create(ctx: Context) {
170
170
  const body = await ctx.body();
171
- return ctx.json({
172
- message: 'Create ${baseName}',
173
- data: body,
174
- }, 201);
171
+ return {
172
+ status: 201,
173
+ data: {
174
+ message: 'Create ${baseName}',
175
+ data: body,
176
+ },
177
+ };
175
178
  }
176
179
 
177
180
  @Put('/:id')
178
181
  async update(ctx: Context) {
179
182
  const { id } = ctx.params;
180
183
  const body = await ctx.body();
181
- return ctx.json({
184
+ return {
182
185
  message: \`Update ${baseName}: \${id}\`,
183
186
  data: body,
184
- });
187
+ };
185
188
  }
186
189
 
187
190
  @Delete('/:id')
188
191
  async delete(ctx: Context) {
189
192
  const { id } = ctx.params;
190
- return ctx.json({
193
+ return {
191
194
  message: \`Delete ${baseName}: \${id}\`,
192
- });
195
+ };
193
196
  }
194
197
  }
195
198
  `;
@@ -271,18 +271,18 @@ app.listen(PORT, () => {
271
271
 
272
272
  export const routes = new Router();
273
273
 
274
- routes.get('/', async (ctx) => {
275
- return ctx.json({
274
+ routes.get('/', async () => {
275
+ return {
276
276
  message: 'Welcome to Nexus!',
277
277
  version: '1.0.0',
278
- });
278
+ };
279
279
  });
280
280
 
281
- routes.get('/health', async (ctx) => {
282
- return ctx.json({
281
+ routes.get('/health', async () => {
282
+ return {
283
283
  status: 'ok',
284
284
  timestamp: new Date().toISOString(),
285
- });
285
+ };
286
286
  });
287
287
  `;
288
288
  }
@@ -320,21 +320,21 @@ import { userRoutes } from './users';
320
320
  export const routes = new Router();
321
321
 
322
322
  // Health check
323
- routes.get('/health', async (ctx) => {
324
- return ctx.json({
323
+ routes.get('/health', async () => {
324
+ return {
325
325
  status: 'ok',
326
326
  timestamp: new Date().toISOString(),
327
327
  uptime: process.uptime(),
328
- });
328
+ };
329
329
  });
330
330
 
331
331
  // API info
332
- routes.get('/api', async (ctx) => {
333
- return ctx.json({
332
+ routes.get('/api', async () => {
333
+ return {
334
334
  name: 'API',
335
335
  version: '1.0.0',
336
336
  endpoints: ['/api/users', '/health'],
337
- });
337
+ };
338
338
  });
339
339
 
340
340
  // Mount user routes
@@ -343,7 +343,7 @@ routes.group('/api/users', userRoutes);
343
343
  }
344
344
 
345
345
  private getUserRoutes(): string {
346
- return `import { Router } from '@engjts/nexus';
346
+ return `import { Router, HttpError } from '@engjts/nexus';
347
347
  import { UserService } from '../services/user.service';
348
348
  import { createUserSchema, updateUserSchema } from '../validators/user.validator';
349
349
 
@@ -351,9 +351,9 @@ export const userRoutes = new Router();
351
351
  const userService = new UserService();
352
352
 
353
353
  // GET /api/users
354
- userRoutes.get('/', async (ctx) => {
354
+ userRoutes.get('/', async () => {
355
355
  const users = await userService.findAll();
356
- return ctx.json({ users });
356
+ return { users };
357
357
  });
358
358
 
359
359
  // GET /api/users/:id
@@ -362,10 +362,10 @@ userRoutes.get('/:id', async (ctx) => {
362
362
  const user = await userService.findById(id);
363
363
 
364
364
  if (!user) {
365
- return ctx.json({ error: 'User not found' }, 404);
365
+ throw new HttpError(404, 'User not found');
366
366
  }
367
367
 
368
- return ctx.json({ user });
368
+ return { user };
369
369
  });
370
370
 
371
371
  // POST /api/users
@@ -374,7 +374,7 @@ userRoutes.post('/', async (ctx) => {
374
374
  const validated = createUserSchema.parse(body);
375
375
 
376
376
  const user = await userService.create(validated);
377
- return ctx.json({ user }, 201);
377
+ return { status: 201, data: { user } };
378
378
  });
379
379
 
380
380
  // PUT /api/users/:id
@@ -386,10 +386,10 @@ userRoutes.put('/:id', async (ctx) => {
386
386
  const user = await userService.update(id, validated);
387
387
 
388
388
  if (!user) {
389
- return ctx.json({ error: 'User not found' }, 404);
389
+ throw new HttpError(404, 'User not found');
390
390
  }
391
391
 
392
- return ctx.json({ user });
392
+ return { user };
393
393
  });
394
394
 
395
395
  // DELETE /api/users/:id
@@ -398,22 +398,22 @@ userRoutes.delete('/:id', async (ctx) => {
398
398
  const deleted = await userService.delete(id);
399
399
 
400
400
  if (!deleted) {
401
- return ctx.json({ error: 'User not found' }, 404);
401
+ throw new HttpError(404, 'User not found');
402
402
  }
403
403
 
404
- return ctx.json({ message: 'User deleted successfully' });
404
+ return { message: 'User deleted successfully' };
405
405
  });
406
406
  `;
407
407
  }
408
408
 
409
409
  private getAuthMiddleware(): string {
410
- return `import { Middleware, Context } from '@engjts/nexus';
410
+ return `import { Middleware, Context, HttpError } from '@engjts/nexus';
411
411
 
412
412
  export const authMiddleware: Middleware = async (ctx: Context, next) => {
413
413
  const authHeader = ctx.headers.get('authorization');
414
414
 
415
415
  if (!authHeader || !authHeader.startsWith('Bearer ')) {
416
- return ctx.json({ error: 'Unauthorized' }, 401);
416
+ throw new HttpError(401, 'Unauthorized');
417
417
  }
418
418
 
419
419
  const token = authHeader.slice(7);
@@ -425,7 +425,7 @@ export const authMiddleware: Middleware = async (ctx: Context, next) => {
425
425
 
426
426
  return next();
427
427
  } catch (error) {
428
- return ctx.json({ error: 'Invalid token' }, 401);
428
+ throw new HttpError(401, 'Invalid token');
429
429
  }
430
430
  };
431
431
  `;