@bloomneo/appkit 1.2.9

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 (262) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +902 -0
  3. package/bin/appkit.js +71 -0
  4. package/bin/commands/generate.js +1050 -0
  5. package/bin/templates/backend/README.md.template +39 -0
  6. package/bin/templates/backend/api.http.template +0 -0
  7. package/bin/templates/backend/docs/APPKIT_CLI.md +507 -0
  8. package/bin/templates/backend/docs/APPKIT_COMMENTS_GUIDELINES.md +61 -0
  9. package/bin/templates/backend/docs/APPKIT_LLM_GUIDE.md +2539 -0
  10. package/bin/templates/backend/package.json.template +34 -0
  11. package/bin/templates/backend/src/api/features/welcome/welcome.http.template +29 -0
  12. package/bin/templates/backend/src/api/features/welcome/welcome.route.ts.template +36 -0
  13. package/bin/templates/backend/src/api/features/welcome/welcome.service.ts.template +88 -0
  14. package/bin/templates/backend/src/api/features/welcome/welcome.types.ts.template +18 -0
  15. package/bin/templates/backend/src/api/lib/api-router.ts.template +84 -0
  16. package/bin/templates/backend/src/api/server.ts.template +188 -0
  17. package/bin/templates/backend/tsconfig.api.json.template +24 -0
  18. package/bin/templates/backend/tsconfig.json.template +40 -0
  19. package/bin/templates/feature/feature.http.template +63 -0
  20. package/bin/templates/feature/feature.route.ts.template +36 -0
  21. package/bin/templates/feature/feature.service.ts.template +81 -0
  22. package/bin/templates/feature/feature.types.ts.template +23 -0
  23. package/bin/templates/feature-db/feature.http.template +63 -0
  24. package/bin/templates/feature-db/feature.model.ts.template +74 -0
  25. package/bin/templates/feature-db/feature.route.ts.template +58 -0
  26. package/bin/templates/feature-db/feature.service.ts.template +231 -0
  27. package/bin/templates/feature-db/feature.types.ts.template +25 -0
  28. package/bin/templates/feature-db/schema-addition.prisma.template +9 -0
  29. package/bin/templates/feature-db/seeding/README.md.template +57 -0
  30. package/bin/templates/feature-db/seeding/feature.seed.js.template +67 -0
  31. package/bin/templates/feature-user/schema-addition.prisma.template +19 -0
  32. package/bin/templates/feature-user/user.http.template +157 -0
  33. package/bin/templates/feature-user/user.model.ts.template +244 -0
  34. package/bin/templates/feature-user/user.route.ts.template +379 -0
  35. package/bin/templates/feature-user/user.seed.js.template +182 -0
  36. package/bin/templates/feature-user/user.service.ts.template +426 -0
  37. package/bin/templates/feature-user/user.types.ts.template +127 -0
  38. package/dist/auth/auth.d.ts +182 -0
  39. package/dist/auth/auth.d.ts.map +1 -0
  40. package/dist/auth/auth.js +477 -0
  41. package/dist/auth/auth.js.map +1 -0
  42. package/dist/auth/defaults.d.ts +104 -0
  43. package/dist/auth/defaults.d.ts.map +1 -0
  44. package/dist/auth/defaults.js +374 -0
  45. package/dist/auth/defaults.js.map +1 -0
  46. package/dist/auth/index.d.ts +70 -0
  47. package/dist/auth/index.d.ts.map +1 -0
  48. package/dist/auth/index.js +94 -0
  49. package/dist/auth/index.js.map +1 -0
  50. package/dist/cache/cache.d.ts +118 -0
  51. package/dist/cache/cache.d.ts.map +1 -0
  52. package/dist/cache/cache.js +249 -0
  53. package/dist/cache/cache.js.map +1 -0
  54. package/dist/cache/defaults.d.ts +63 -0
  55. package/dist/cache/defaults.d.ts.map +1 -0
  56. package/dist/cache/defaults.js +193 -0
  57. package/dist/cache/defaults.js.map +1 -0
  58. package/dist/cache/index.d.ts +101 -0
  59. package/dist/cache/index.d.ts.map +1 -0
  60. package/dist/cache/index.js +203 -0
  61. package/dist/cache/index.js.map +1 -0
  62. package/dist/cache/strategies/memory.d.ts +138 -0
  63. package/dist/cache/strategies/memory.d.ts.map +1 -0
  64. package/dist/cache/strategies/memory.js +348 -0
  65. package/dist/cache/strategies/memory.js.map +1 -0
  66. package/dist/cache/strategies/redis.d.ts +105 -0
  67. package/dist/cache/strategies/redis.d.ts.map +1 -0
  68. package/dist/cache/strategies/redis.js +318 -0
  69. package/dist/cache/strategies/redis.js.map +1 -0
  70. package/dist/config/config.d.ts +62 -0
  71. package/dist/config/config.d.ts.map +1 -0
  72. package/dist/config/config.js +107 -0
  73. package/dist/config/config.js.map +1 -0
  74. package/dist/config/defaults.d.ts +44 -0
  75. package/dist/config/defaults.d.ts.map +1 -0
  76. package/dist/config/defaults.js +217 -0
  77. package/dist/config/defaults.js.map +1 -0
  78. package/dist/config/index.d.ts +105 -0
  79. package/dist/config/index.d.ts.map +1 -0
  80. package/dist/config/index.js +163 -0
  81. package/dist/config/index.js.map +1 -0
  82. package/dist/database/adapters/mongoose.d.ts +106 -0
  83. package/dist/database/adapters/mongoose.d.ts.map +1 -0
  84. package/dist/database/adapters/mongoose.js +480 -0
  85. package/dist/database/adapters/mongoose.js.map +1 -0
  86. package/dist/database/adapters/prisma.d.ts +106 -0
  87. package/dist/database/adapters/prisma.d.ts.map +1 -0
  88. package/dist/database/adapters/prisma.js +494 -0
  89. package/dist/database/adapters/prisma.js.map +1 -0
  90. package/dist/database/defaults.d.ts +87 -0
  91. package/dist/database/defaults.d.ts.map +1 -0
  92. package/dist/database/defaults.js +271 -0
  93. package/dist/database/defaults.js.map +1 -0
  94. package/dist/database/index.d.ts +137 -0
  95. package/dist/database/index.d.ts.map +1 -0
  96. package/dist/database/index.js +490 -0
  97. package/dist/database/index.js.map +1 -0
  98. package/dist/email/defaults.d.ts +100 -0
  99. package/dist/email/defaults.d.ts.map +1 -0
  100. package/dist/email/defaults.js +400 -0
  101. package/dist/email/defaults.js.map +1 -0
  102. package/dist/email/email.d.ts +139 -0
  103. package/dist/email/email.d.ts.map +1 -0
  104. package/dist/email/email.js +316 -0
  105. package/dist/email/email.js.map +1 -0
  106. package/dist/email/index.d.ts +176 -0
  107. package/dist/email/index.d.ts.map +1 -0
  108. package/dist/email/index.js +251 -0
  109. package/dist/email/index.js.map +1 -0
  110. package/dist/email/strategies/console.d.ts +90 -0
  111. package/dist/email/strategies/console.d.ts.map +1 -0
  112. package/dist/email/strategies/console.js +268 -0
  113. package/dist/email/strategies/console.js.map +1 -0
  114. package/dist/email/strategies/resend.d.ts +84 -0
  115. package/dist/email/strategies/resend.d.ts.map +1 -0
  116. package/dist/email/strategies/resend.js +266 -0
  117. package/dist/email/strategies/resend.js.map +1 -0
  118. package/dist/email/strategies/smtp.d.ts +77 -0
  119. package/dist/email/strategies/smtp.d.ts.map +1 -0
  120. package/dist/email/strategies/smtp.js +286 -0
  121. package/dist/email/strategies/smtp.js.map +1 -0
  122. package/dist/error/defaults.d.ts +40 -0
  123. package/dist/error/defaults.d.ts.map +1 -0
  124. package/dist/error/defaults.js +75 -0
  125. package/dist/error/defaults.js.map +1 -0
  126. package/dist/error/error.d.ts +140 -0
  127. package/dist/error/error.d.ts.map +1 -0
  128. package/dist/error/error.js +200 -0
  129. package/dist/error/error.js.map +1 -0
  130. package/dist/error/index.d.ts +145 -0
  131. package/dist/error/index.d.ts.map +1 -0
  132. package/dist/error/index.js +145 -0
  133. package/dist/error/index.js.map +1 -0
  134. package/dist/event/defaults.d.ts +111 -0
  135. package/dist/event/defaults.d.ts.map +1 -0
  136. package/dist/event/defaults.js +378 -0
  137. package/dist/event/defaults.js.map +1 -0
  138. package/dist/event/event.d.ts +171 -0
  139. package/dist/event/event.d.ts.map +1 -0
  140. package/dist/event/event.js +391 -0
  141. package/dist/event/event.js.map +1 -0
  142. package/dist/event/index.d.ts +173 -0
  143. package/dist/event/index.d.ts.map +1 -0
  144. package/dist/event/index.js +302 -0
  145. package/dist/event/index.js.map +1 -0
  146. package/dist/event/strategies/memory.d.ts +122 -0
  147. package/dist/event/strategies/memory.d.ts.map +1 -0
  148. package/dist/event/strategies/memory.js +331 -0
  149. package/dist/event/strategies/memory.js.map +1 -0
  150. package/dist/event/strategies/redis.d.ts +115 -0
  151. package/dist/event/strategies/redis.d.ts.map +1 -0
  152. package/dist/event/strategies/redis.js +434 -0
  153. package/dist/event/strategies/redis.js.map +1 -0
  154. package/dist/index.d.ts +58 -0
  155. package/dist/index.d.ts.map +1 -0
  156. package/dist/index.js +72 -0
  157. package/dist/index.js.map +1 -0
  158. package/dist/logger/defaults.d.ts +67 -0
  159. package/dist/logger/defaults.d.ts.map +1 -0
  160. package/dist/logger/defaults.js +213 -0
  161. package/dist/logger/defaults.js.map +1 -0
  162. package/dist/logger/index.d.ts +84 -0
  163. package/dist/logger/index.d.ts.map +1 -0
  164. package/dist/logger/index.js +101 -0
  165. package/dist/logger/index.js.map +1 -0
  166. package/dist/logger/logger.d.ts +165 -0
  167. package/dist/logger/logger.d.ts.map +1 -0
  168. package/dist/logger/logger.js +843 -0
  169. package/dist/logger/logger.js.map +1 -0
  170. package/dist/logger/transports/console.d.ts +102 -0
  171. package/dist/logger/transports/console.d.ts.map +1 -0
  172. package/dist/logger/transports/console.js +276 -0
  173. package/dist/logger/transports/console.js.map +1 -0
  174. package/dist/logger/transports/database.d.ts +153 -0
  175. package/dist/logger/transports/database.d.ts.map +1 -0
  176. package/dist/logger/transports/database.js +539 -0
  177. package/dist/logger/transports/database.js.map +1 -0
  178. package/dist/logger/transports/file.d.ts +146 -0
  179. package/dist/logger/transports/file.d.ts.map +1 -0
  180. package/dist/logger/transports/file.js +464 -0
  181. package/dist/logger/transports/file.js.map +1 -0
  182. package/dist/logger/transports/http.d.ts +128 -0
  183. package/dist/logger/transports/http.d.ts.map +1 -0
  184. package/dist/logger/transports/http.js +401 -0
  185. package/dist/logger/transports/http.js.map +1 -0
  186. package/dist/logger/transports/webhook.d.ts +152 -0
  187. package/dist/logger/transports/webhook.d.ts.map +1 -0
  188. package/dist/logger/transports/webhook.js +485 -0
  189. package/dist/logger/transports/webhook.js.map +1 -0
  190. package/dist/queue/defaults.d.ts +66 -0
  191. package/dist/queue/defaults.d.ts.map +1 -0
  192. package/dist/queue/defaults.js +205 -0
  193. package/dist/queue/defaults.js.map +1 -0
  194. package/dist/queue/index.d.ts +124 -0
  195. package/dist/queue/index.d.ts.map +1 -0
  196. package/dist/queue/index.js +116 -0
  197. package/dist/queue/index.js.map +1 -0
  198. package/dist/queue/queue.d.ts +156 -0
  199. package/dist/queue/queue.d.ts.map +1 -0
  200. package/dist/queue/queue.js +387 -0
  201. package/dist/queue/queue.js.map +1 -0
  202. package/dist/queue/transports/database.d.ts +165 -0
  203. package/dist/queue/transports/database.d.ts.map +1 -0
  204. package/dist/queue/transports/database.js +595 -0
  205. package/dist/queue/transports/database.js.map +1 -0
  206. package/dist/queue/transports/memory.d.ts +143 -0
  207. package/dist/queue/transports/memory.d.ts.map +1 -0
  208. package/dist/queue/transports/memory.js +415 -0
  209. package/dist/queue/transports/memory.js.map +1 -0
  210. package/dist/queue/transports/redis.d.ts +203 -0
  211. package/dist/queue/transports/redis.d.ts.map +1 -0
  212. package/dist/queue/transports/redis.js +744 -0
  213. package/dist/queue/transports/redis.js.map +1 -0
  214. package/dist/security/defaults.d.ts +64 -0
  215. package/dist/security/defaults.d.ts.map +1 -0
  216. package/dist/security/defaults.js +159 -0
  217. package/dist/security/defaults.js.map +1 -0
  218. package/dist/security/index.d.ts +110 -0
  219. package/dist/security/index.d.ts.map +1 -0
  220. package/dist/security/index.js +160 -0
  221. package/dist/security/index.js.map +1 -0
  222. package/dist/security/security.d.ts +138 -0
  223. package/dist/security/security.d.ts.map +1 -0
  224. package/dist/security/security.js +419 -0
  225. package/dist/security/security.js.map +1 -0
  226. package/dist/storage/defaults.d.ts +79 -0
  227. package/dist/storage/defaults.d.ts.map +1 -0
  228. package/dist/storage/defaults.js +358 -0
  229. package/dist/storage/defaults.js.map +1 -0
  230. package/dist/storage/index.d.ts +153 -0
  231. package/dist/storage/index.d.ts.map +1 -0
  232. package/dist/storage/index.js +242 -0
  233. package/dist/storage/index.js.map +1 -0
  234. package/dist/storage/storage.d.ts +151 -0
  235. package/dist/storage/storage.d.ts.map +1 -0
  236. package/dist/storage/storage.js +439 -0
  237. package/dist/storage/storage.js.map +1 -0
  238. package/dist/storage/strategies/local.d.ts +117 -0
  239. package/dist/storage/strategies/local.d.ts.map +1 -0
  240. package/dist/storage/strategies/local.js +368 -0
  241. package/dist/storage/strategies/local.js.map +1 -0
  242. package/dist/storage/strategies/r2.d.ts +130 -0
  243. package/dist/storage/strategies/r2.d.ts.map +1 -0
  244. package/dist/storage/strategies/r2.js +470 -0
  245. package/dist/storage/strategies/r2.js.map +1 -0
  246. package/dist/storage/strategies/s3.d.ts +121 -0
  247. package/dist/storage/strategies/s3.d.ts.map +1 -0
  248. package/dist/storage/strategies/s3.js +461 -0
  249. package/dist/storage/strategies/s3.js.map +1 -0
  250. package/dist/util/defaults.d.ts +77 -0
  251. package/dist/util/defaults.d.ts.map +1 -0
  252. package/dist/util/defaults.js +193 -0
  253. package/dist/util/defaults.js.map +1 -0
  254. package/dist/util/index.d.ts +97 -0
  255. package/dist/util/index.d.ts.map +1 -0
  256. package/dist/util/index.js +165 -0
  257. package/dist/util/index.js.map +1 -0
  258. package/dist/util/util.d.ts +145 -0
  259. package/dist/util/util.d.ts.map +1 -0
  260. package/dist/util/util.js +481 -0
  261. package/dist/util/util.js.map +1 -0
  262. package/package.json +234 -0
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "{{projectName}}",
3
+ "version": "1.0.0",
4
+ "description": "Backend API with FBCA pattern",
5
+ "type": "module",
6
+ "main": "src/api/server.ts",
7
+ "scripts": {
8
+ "dev": "nodemon --exec tsx src/api/server.ts",
9
+ "start": "tsx src/api/server.ts",
10
+ "build": "tsc",
11
+ "start:prod": "node dist/api/server.js"
12
+ },
13
+ "keywords": [
14
+ "api",
15
+ "backend",
16
+ "fbca"
17
+ ],
18
+ "author": "",
19
+ "license": "ISC",
20
+ "dependencies": {
21
+ "@bloomneo/appkit": "^1.0.0",
22
+ "cors": "^2.8.5",
23
+ "dotenv": "^16.4.5",
24
+ "express": "^4.18.2"
25
+ },
26
+ "devDependencies": {
27
+ "@types/cors": "^2.8.19",
28
+ "@types/express": "^5.0.3",
29
+ "@types/node": "^24.4.0",
30
+ "nodemon": "^3.0.1",
31
+ "tsx": "^4.20.5",
32
+ "typescript": "^5.9.2"
33
+ }
34
+ }
@@ -0,0 +1,29 @@
1
+ ### Welcome API Testing with Frontend Key
2
+ ### Base URL: http://localhost:3000
3
+ ### Note: In development, X-Frontend-Key is not required but shown for testing
4
+
5
+ ### 1. Basic hello (without key - should work in dev)
6
+ GET http://localhost:3000/api/welcome
7
+
8
+ ###
9
+
10
+ ### 2. Basic hello (with frontend key)
11
+ GET http://localhost:3000/api/welcome
12
+ X-Frontend-Key: {{frontendKey}}
13
+
14
+ ###
15
+
16
+ ### 3. Test with wrong key (should fail in production)
17
+ GET http://localhost:3000/api/welcome
18
+ X-Frontend-Key: wrong_key_123
19
+
20
+ ###
21
+
22
+ ### Health Check (not protected)
23
+ GET http://localhost:3000/health
24
+
25
+ ###
26
+
27
+ ### API Info (protected)
28
+ GET http://localhost:3000/api
29
+ X-Frontend-Key: {{frontendKey}}
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Welcome Feature Routes - Express endpoints with AppKit integration
3
+ * @module {{projectName}}/welcome-routes
4
+ * @file {{projectName}}/src/api/features/{featureName}/{featureName}.route.ts
5
+ *
6
+ * @llm-rule WHEN: Need HTTP endpoints for welcome feature with error handling
7
+ * @llm-rule AVOID: Adding routes without asyncRoute wrapper - breaks error handling
8
+ * @llm-rule NOTE: Auto-discovered by api-router.ts using FBCA pattern, exports default Express router
9
+ */
10
+
11
+ import express from 'express';
12
+ import { errorClass } from '@bloomneo/appkit/error';
13
+ import { loggerClass } from '@bloomneo/appkit/logger';
14
+ import { welcomeService } from './welcome.service.js';
15
+
16
+ const router = express.Router();
17
+ const error = errorClass.get();
18
+ const logger = loggerClass.get('welcome-routes');
19
+
20
+ // GET /api/{featureName} - replies "hello"
21
+ router.get('/', error.asyncRoute(async (_req, res) => {
22
+ logger.info('GET /api/{featureName} request received');
23
+ const result = await welcomeService.getHello();
24
+ res.json(result);
25
+ }));
26
+
27
+ // GET /api/{featureName}/:name - replies "hello :name"
28
+ router.get('/:name', error.asyncRoute(async (req, res) => {
29
+ const name = req.params.name;
30
+ logger.info('GET /api/{featureName}/:name request received', { name });
31
+
32
+ const result = await welcomeService.getHelloWithName(name);
33
+ res.json(result);
34
+ }));
35
+
36
+ export default router;
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Welcome Feature Service - Business logic with AppKit integration
3
+ * @module {{projectName}}/welcome-service
4
+ * @file {{projectName}}/src/api/features/{featureName}/{featureName}.service.ts
5
+ *
6
+ * @llm-rule WHEN: Need business logic layer with validation, logging, and config
7
+ * @llm-rule AVOID: Direct database calls from routes - always use service layer
8
+ * @llm-rule NOTE: Demonstrates AppKit logger, config, and error patterns for FBCA
9
+ */
10
+
11
+ import { loggerClass } from '@bloomneo/appkit/logger';
12
+ import { configClass } from '@bloomneo/appkit/config';
13
+ import { errorClass } from '@bloomneo/appkit/error';
14
+ import type { WelcomeResponse, PersonalizedWelcomeResponse } from './welcome.types.js';
15
+
16
+ // Initialize AppKit modules following the pattern
17
+ const logger = loggerClass.get('welcome');
18
+ const config = configClass.get();
19
+ const error = errorClass.get();
20
+
21
+ export const welcomeService = {
22
+ /**
23
+ * Get basic hello message
24
+ */
25
+ async getHello(): Promise<WelcomeResponse> {
26
+ try {
27
+ logger.info('Processing basic hello request');
28
+
29
+ // Get default welcome from config (with fallback)
30
+ const defaultWelcome = config.get('welcome.default', 'hello') as string;
31
+
32
+ const result: WelcomeResponse = {
33
+ message: defaultWelcome,
34
+ timestamp: new Date().toISOString()
35
+ };
36
+
37
+ logger.info('Basic hello request completed', { result });
38
+ return result;
39
+
40
+ } catch (err) {
41
+ logger.error('Failed to process basic hello request', { error: err });
42
+ throw error.serverError('Failed to generate welcome message');
43
+ }
44
+ },
45
+
46
+ /**
47
+ * Get hello message with name
48
+ */
49
+ async getHelloWithName(name: string): Promise<PersonalizedWelcomeResponse> {
50
+ try {
51
+ logger.info('Processing personalized hello request', { name });
52
+
53
+ // Validate input
54
+ if (!name || typeof name !== 'string') {
55
+ logger.warn('Invalid name provided', { name });
56
+ throw error.badRequest('Name must be a valid string');
57
+ }
58
+
59
+ if (name.length > 100) {
60
+ logger.warn('Name too long', { name, length: name.length });
61
+ throw error.badRequest('Name must be less than 100 characters');
62
+ }
63
+
64
+ // Get welcome format from config
65
+ const welcomeFormat = config.get('welcome.format', 'hello {name}') as string;
66
+ const message = welcomeFormat.replace('{name}', name);
67
+
68
+ const result = {
69
+ message,
70
+ name,
71
+ timestamp: new Date().toISOString()
72
+ };
73
+
74
+ logger.info('Personalized hello request completed', { name, result });
75
+ return result;
76
+
77
+ } catch (err: any) {
78
+ // Re-throw AppKit errors as-is
79
+ if (err.statusCode) {
80
+ throw err;
81
+ }
82
+
83
+ // Convert other errors to server errors
84
+ logger.error('Failed to process personalized hello request', { name, error: err });
85
+ throw error.serverError('Failed to generate personalized welcome message');
86
+ }
87
+ }
88
+ };
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Welcome Feature Type Definitions - Shared interfaces and types
3
+ * @module {{projectName}}/welcome-types
4
+ * @file {{projectName}}/src/api/features/{featureName}/{featureName}.types.ts
5
+ *
6
+ * @llm-rule WHEN: Need type safety for welcome feature data structures
7
+ * @llm-rule AVOID: Defining types inline - reduces reusability and consistency
8
+ * @llm-rule NOTE: Shared across service and route layers for type consistency
9
+ */
10
+
11
+ export interface WelcomeResponse {
12
+ message: string;
13
+ timestamp: string;
14
+ }
15
+
16
+ export interface PersonalizedWelcomeResponse extends WelcomeResponse {
17
+ name: string;
18
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * FBCA Auto-Discovery Router
3
+ * @module {{projectName}}/api-router
4
+ * @file {{projectName}}/src/api/lib/api-router.ts
5
+ *
6
+ * @llm-rule WHEN: Need automatic API route discovery based on feature directories
7
+ * @llm-rule AVOID: Manual route registration - defeats FBCA auto-discovery purpose
8
+ * @llm-rule NOTE: Follows {featureName}/{featureName}.route.ts naming convention for TypeScript or .js for JavaScript
9
+ */
10
+
11
+ import express from 'express';
12
+ import { fileURLToPath, pathToFileURL } from 'url';
13
+ import { dirname, join } from 'path';
14
+ import { readdir } from 'fs/promises';
15
+ import { loggerClass } from '@bloomneo/appkit/logger';
16
+
17
+ const __filename = fileURLToPath(import.meta.url);
18
+ const __dirname = dirname(__filename);
19
+
20
+ export async function createApiRouter() {
21
+ const router = express.Router();
22
+ const discoveredRoutes: string[] = [];
23
+ const logger = loggerClass.get('api-router');
24
+
25
+ // API root route - list available endpoints
26
+ router.get('/', (_req, res) => {
27
+ res.json({
28
+ message: 'API Server - Feature-Based Component Architecture',
29
+ version: '1.0.0',
30
+ endpoints: {
31
+ health: '/health',
32
+ api: '/api',
33
+ features: discoveredRoutes.map(route => `/api${route}`)
34
+ },
35
+ timestamp: new Date().toISOString()
36
+ });
37
+ });
38
+
39
+ try {
40
+ // Auto-discover feature routes
41
+ const featuresPath = join(__dirname, '../features');
42
+ const features = await readdir(featuresPath, { withFileTypes: true });
43
+
44
+ logger.info('🔍 Discovering API routes:');
45
+
46
+ for (const feature of features) {
47
+ if (feature.isDirectory()) {
48
+ const featureName = feature.name;
49
+ // Try .ts first, then .js
50
+ const routeFiles = [
51
+ join(featuresPath, featureName, `${featureName}.route.ts`),
52
+ join(featuresPath, featureName, `${featureName}.route.js`)
53
+ ];
54
+
55
+ let loaded = false;
56
+ for (const routeFile of routeFiles) {
57
+ try {
58
+ const fileUrl = pathToFileURL(routeFile).href;
59
+ const { default: featureRouter } = await import(fileUrl);
60
+ router.use(`/${featureName}`, featureRouter);
61
+ discoveredRoutes.push(`/${featureName}`);
62
+ const fileType = routeFile.endsWith('.ts') ? '.ts' : '.js';
63
+ logger.info(` /api/${featureName} -> ${featureName}.route${fileType}`);
64
+ loaded = true;
65
+ break;
66
+ } catch (error) {
67
+ // Continue to next file type
68
+ }
69
+ }
70
+
71
+ if (!loaded) {
72
+ logger.warn(`⚠️ Could not load route for feature "${featureName}"`);
73
+ }
74
+ }
75
+ }
76
+
77
+ logger.info('✅ API routes discovered');
78
+
79
+ } catch (error) {
80
+ logger.warn('⚠️ No features directory found');
81
+ }
82
+
83
+ return router;
84
+ }
@@ -0,0 +1,188 @@
1
+ /**
2
+ * FBCA Backend API Server with AppKit integration
3
+ * @module {{projectName}}/server
4
+ * @file {{projectName}}/src/api/server.ts
5
+ *
6
+ * @llm-rule WHEN: Creating backend APIs with Feature-Based Component Architecture
7
+ * @llm-rule AVOID: Using without AppKit modules - breaks structured logging and error handling
8
+ * @llm-rule NOTE: Auto-discovers features in features/ directory using naming convention
9
+ */
10
+
11
+ import 'dotenv/config';
12
+ import express from 'express';
13
+ import cors from 'cors';
14
+ import path from 'path';
15
+ import fs from 'fs';
16
+ import { fileURLToPath } from 'url';
17
+ import { loggerClass } from '@bloomneo/appkit/logger';
18
+ import { errorClass } from '@bloomneo/appkit/error';
19
+ import { configClass } from '@bloomneo/appkit/config';
20
+ import { createApiRouter } from './lib/api-router.js';
21
+
22
+ const __filename = fileURLToPath(import.meta.url);
23
+ const __dirname = path.dirname(__filename);
24
+
25
+ // Initialize AppKit modules following the pattern
26
+ const logger = loggerClass.get('server');
27
+ const error = errorClass.get();
28
+ const config = configClass.get();
29
+
30
+ /**
31
+ * Check if frontend build exists
32
+ */
33
+ function checkFrontendExists(distPath: string): boolean {
34
+ try {
35
+ const indexPath = path.join(distPath, 'index.html');
36
+ return fs.existsSync(indexPath);
37
+ } catch {
38
+ return false;
39
+ }
40
+ }
41
+
42
+ const app = express();
43
+ const PORT = config.get('server.port', process.env.PORT || 3000);
44
+
45
+ // Middleware (following AppKit recommended order)
46
+ app.use(cors());
47
+ app.use(express.json());
48
+ app.use(express.urlencoded({ extended: true }));
49
+
50
+ // Request logging middleware
51
+ app.use((req, _res, next) => {
52
+ logger.info('Incoming request', {
53
+ method: req.method,
54
+ url: req.url,
55
+ userAgent: req.get('User-Agent'),
56
+ ip: req.ip
57
+ });
58
+ next();
59
+ });
60
+
61
+ // Frontend key protection for API routes (excluding /api root and /health)
62
+ app.use('/api', (req, res, next) => {
63
+ // Skip for specific endpoints
64
+ if (req.path === '/' || req.path === '') {
65
+ logger.info('🔓 Frontend key check skipped for /api root');
66
+ return next();
67
+ }
68
+
69
+ // Skip in development environment
70
+ if (process.env.NODE_ENV === 'development') {
71
+ logger.info('🔓 Frontend key check skipped (development mode)');
72
+ return next();
73
+ }
74
+
75
+ const frontendKey = req.headers['x-frontend-key'] as string;
76
+ const expectedKey = process.env.VOILA_FRONTEND_KEY;
77
+
78
+ // Warn if no key is configured in production
79
+ if (!expectedKey) {
80
+ logger.warn('⚠️ VOILA_FRONTEND_KEY not configured in production environment');
81
+ return next(); // Fail gracefully
82
+ }
83
+
84
+ // Check if frontend key is provided and matches
85
+ if (!frontendKey) {
86
+ logger.warn('❌ Frontend key missing in request headers', { url: req.url });
87
+ return res.status(403).json({
88
+ error: 'Frontend access key required',
89
+ message: 'API access requires valid frontend key in X-Frontend-Key header'
90
+ });
91
+ }
92
+
93
+ if (frontendKey !== expectedKey) {
94
+ logger.warn('❌ Invalid frontend key provided', {
95
+ url: req.url,
96
+ providedKey: frontendKey.substring(0, 8) + '...'
97
+ });
98
+ return res.status(403).json({
99
+ error: 'Invalid frontend access key',
100
+ message: 'The provided frontend key is not valid'
101
+ });
102
+ }
103
+
104
+ logger.info('✅ Frontend key validated successfully', { url: req.url });
105
+ next();
106
+ });
107
+
108
+ // Health check with AppKit integration
109
+ app.get('/health', error.asyncRoute(async (_req, res) => {
110
+ logger.info('Health check requested');
111
+
112
+ const healthData = {
113
+ status: 'ok',
114
+ timestamp: new Date().toISOString(),
115
+ uptime: process.uptime(),
116
+ environment: config.get('app.environment', 'development')
117
+ };
118
+
119
+ res.json(healthData);
120
+ }));
121
+
122
+ // Initialize server with async setup
123
+ async function startServer() {
124
+ try {
125
+ logger.info('Initializing server...');
126
+
127
+ // API routes with auto-discovery
128
+ app.use('/api', await createApiRouter());
129
+
130
+ // Check if frontend build exists and we're not in API-only dev mode
131
+ const distPath = path.join(__dirname, '../../dist');
132
+ const frontendExists = checkFrontendExists(distPath);
133
+ const isApiOnlyMode = process.env.NODE_ENV !== 'production' && process.env.API_ONLY === 'true';
134
+
135
+ logger.info('Frontend check:', { distPath, frontendExists, isApiOnlyMode });
136
+
137
+ if (frontendExists && !isApiOnlyMode) {
138
+ // Serve static files from dist directory (production frontend)
139
+ logger.info('🌐 Serving frontend from dist directory');
140
+ app.use(express.static(distPath));
141
+
142
+ // SPA fallback - serve index.html for all non-API routes
143
+ app.get('*', (req, res) => {
144
+ const indexPath = path.join(distPath, 'index.html');
145
+ logger.info('Serving SPA route', { path: req.path });
146
+ res.sendFile(indexPath, (err) => {
147
+ if (err) {
148
+ logger.error('Failed to serve index.html', { error: err });
149
+ res.status(404).json({
150
+ error: 'Frontend not found',
151
+ message: 'Frontend files exist but failed to serve'
152
+ });
153
+ }
154
+ });
155
+ });
156
+ } else {
157
+ // Fallback to /api route when no frontend
158
+ logger.info('🔧 No frontend found, redirecting root to /api');
159
+ app.get('/', (_req, res) => {
160
+ logger.info('Root route requested, redirecting to /api');
161
+ res.redirect('/api');
162
+ });
163
+ }
164
+
165
+ // AppKit error handling middleware (ALWAYS LAST)
166
+ app.use(error.handleErrors());
167
+
168
+ app.listen(PORT, () => {
169
+ if (frontendExists) {
170
+ logger.info(`🚀 Full-stack server running on http://localhost:${PORT}`);
171
+ logger.info(`🌐 Frontend: http://localhost:${PORT}`);
172
+ } else {
173
+ logger.info(`🔧 API-only server running on http://localhost:${PORT}`);
174
+ logger.info(`🌐 Root: http://localhost:${PORT} → redirects to /api`);
175
+ }
176
+ logger.info(`📚 API routes: http://localhost:${PORT}/api`);
177
+ logger.info(`💊 Health check: http://localhost:${PORT}/health`);
178
+ logger.info('Server initialization completed successfully');
179
+ });
180
+
181
+ } catch (err: any) {
182
+ logger.error('Failed to start server', { error: err });
183
+ logger.error('❌ Server startup failed:', { error: err.message });
184
+ process.exit(1);
185
+ }
186
+ }
187
+
188
+ startServer();
@@ -0,0 +1,24 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "lib": ["ES2023"],
5
+ "module": "Node16",
6
+ "skipLibCheck": true,
7
+ "moduleResolution": "node16",
8
+ "resolveJsonModule": true,
9
+ "outDir": "./dist",
10
+ "rootDir": "./src",
11
+ "strict": false,
12
+ "esModuleInterop": true,
13
+ "allowSyntheticDefaultImports": true,
14
+ "forceConsistentCasingInFileNames": true
15
+ },
16
+ "include": [
17
+ "src/api/**/*"
18
+ ],
19
+ "exclude": [
20
+ "node_modules",
21
+ "dist",
22
+ "src/web"
23
+ ]
24
+ }
@@ -0,0 +1,40 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "allowSyntheticDefaultImports": true,
7
+ "esModuleInterop": true,
8
+ "allowJs": true,
9
+ "checkJs": false,
10
+ "declaration": false,
11
+ "outDir": "./dist",
12
+ "rootDir": "./src",
13
+ "removeComments": false,
14
+ "strict": true,
15
+ "noImplicitAny": true,
16
+ "strictNullChecks": true,
17
+ "strictFunctionTypes": true,
18
+ "noImplicitReturns": true,
19
+ "noFallthroughCasesInSwitch": true,
20
+ "noImplicitOverride": true,
21
+ "exactOptionalPropertyTypes": false,
22
+ "noPropertyAccessFromIndexSignature": false,
23
+ "forceConsistentCasingInFileNames": true,
24
+ "skipLibCheck": true,
25
+ "resolveJsonModule": true,
26
+ "types": ["node"]
27
+ },
28
+ "include": [
29
+ "src/**/*"
30
+ ],
31
+ "exclude": [
32
+ "node_modules",
33
+ "dist",
34
+ "**/*.test.ts",
35
+ "**/*.spec.ts"
36
+ ],
37
+ "ts-node": {
38
+ "esm": true
39
+ }
40
+ }
@@ -0,0 +1,63 @@
1
+ ### {{FeatureName}} API Testing with Frontend Key
2
+ ### Base URL: http://localhost:3000
3
+ ### Note: In development, X-Frontend-Key is not required but shown for testing
4
+
5
+ ### 1. Get all {{featureName}} (without key - should work in dev)
6
+ GET http://localhost:3000/api/{{featureName}}
7
+
8
+ ###
9
+
10
+ ### 2. Get all {{featureName}} (with frontend key)
11
+ GET http://localhost:3000/api/{{featureName}}
12
+ X-Frontend-Key: {{frontendKey}}
13
+
14
+ ###
15
+
16
+ ### 3. Create a new {{featureName}} (with frontend key)
17
+ POST http://localhost:3000/api/{{featureName}}
18
+ Content-Type: application/json
19
+ X-Frontend-Key: {{frontendKey}}
20
+
21
+ {
22
+ "name": "Sample {{FeatureName}}"
23
+ }
24
+
25
+ ###
26
+
27
+ ### 4. Get {{featureName}} by ID (with frontend key)
28
+ GET http://localhost:3000/api/{{featureName}}/1
29
+ X-Frontend-Key: {{frontendKey}}
30
+
31
+ ###
32
+
33
+ ### 5. Update {{featureName}} by ID (with frontend key)
34
+ PUT http://localhost:3000/api/{{featureName}}/1
35
+ Content-Type: application/json
36
+ X-Frontend-Key: {{frontendKey}}
37
+
38
+ {
39
+ "name": "Updated {{FeatureName}}"
40
+ }
41
+
42
+ ###
43
+
44
+ ### 6. Delete {{featureName}} by ID (with frontend key)
45
+ DELETE http://localhost:3000/api/{{featureName}}/1
46
+ X-Frontend-Key: {{frontendKey}}
47
+
48
+ ###
49
+
50
+ ### 7. Test with wrong key (should fail in production)
51
+ GET http://localhost:3000/api/{{featureName}}
52
+ X-Frontend-Key: wrong_key_123
53
+
54
+ ###
55
+
56
+ ### Health Check (not protected)
57
+ GET http://localhost:3000/health
58
+
59
+ ###
60
+
61
+ ### API Info (protected)
62
+ GET http://localhost:3000/api
63
+ X-Frontend-Key: {{frontendKey}}
@@ -0,0 +1,36 @@
1
+ /**
2
+ * {{featureName}} Feature Routes - Express endpoints with AppKit integration
3
+ * @module {{projectName}}/{{featureName}}-routes
4
+ * @file src/api/features/{{featureName}}/{{featureName}}.route.ts
5
+ *
6
+ * @llm-rule WHEN: Need HTTP endpoints for {{featureName}} feature with error handling
7
+ * @llm-rule AVOID: Adding routes without asyncRoute wrapper - breaks error handling
8
+ * @llm-rule NOTE: Auto-discovered by router.ts, exports default Express router
9
+ */
10
+
11
+ import express from 'express';
12
+ import { errorClass } from '@bloomneo/appkit/error';
13
+ import { loggerClass } from '@bloomneo/appkit/logger';
14
+ import { {{featureName}}Service } from './{{featureName}}.service.js';
15
+
16
+ const router = express.Router();
17
+ const error = errorClass.get();
18
+ const logger = loggerClass.get('{{featureName}}-routes');
19
+
20
+ // GET /api/{{featureName}} - Basic {{featureName}} endpoint
21
+ router.get('/', error.asyncRoute(async (_req, res) => {
22
+ logger.info('GET /api/{{featureName}} request received');
23
+ const result = await {{featureName}}Service.getAll();
24
+ res.json(result);
25
+ }));
26
+
27
+ // GET /api/{{featureName}}/:id - Get specific {{featureName}} by ID
28
+ router.get('/:id', error.asyncRoute(async (req, res) => {
29
+ const id = req.params.id;
30
+ logger.info('GET /api/{{featureName}}/:id request received', { id });
31
+
32
+ const result = await {{featureName}}Service.getById(id);
33
+ res.json(result);
34
+ }));
35
+
36
+ export default router;