nodejs-quickstart-structure 1.14.0 → 1.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/bin/index.js +84 -80
  3. package/lib/generator.js +11 -1
  4. package/lib/modules/app-setup.js +3 -3
  5. package/lib/modules/config-files.js +27 -2
  6. package/lib/modules/kafka-setup.js +70 -24
  7. package/package.json +8 -3
  8. package/templates/clean-architecture/js/src/index.js.ejs +1 -3
  9. package/templates/clean-architecture/js/src/infrastructure/webserver/server.js.ejs +11 -10
  10. package/templates/clean-architecture/js/src/interfaces/controllers/userController.js.ejs +16 -1
  11. package/templates/clean-architecture/js/src/interfaces/controllers/userController.spec.js.ejs +36 -0
  12. package/templates/clean-architecture/ts/src/config/swagger.ts.ejs +1 -1
  13. package/templates/clean-architecture/ts/src/index.ts.ejs +12 -14
  14. package/templates/clean-architecture/ts/src/infrastructure/log/logger.spec.ts.ejs +0 -1
  15. package/templates/clean-architecture/ts/src/interfaces/controllers/userController.spec.ts.ejs +19 -0
  16. package/templates/clean-architecture/ts/src/interfaces/controllers/userController.ts.ejs +16 -0
  17. package/templates/common/.cursorrules.ejs +60 -0
  18. package/templates/common/.dockerignore +2 -0
  19. package/templates/common/.gitlab-ci.yml.ejs +5 -5
  20. package/templates/common/Dockerfile +2 -0
  21. package/templates/common/Jenkinsfile.ejs +1 -1
  22. package/templates/common/README.md.ejs +34 -1
  23. package/templates/common/_github/workflows/ci.yml +7 -4
  24. package/templates/common/database/js/models/User.js.ejs +2 -1
  25. package/templates/common/database/ts/models/User.ts.ejs +4 -3
  26. package/templates/common/eslint.config.mjs.ejs +30 -3
  27. package/templates/common/jest.config.js.ejs +4 -1
  28. package/templates/common/kafka/js/messaging/baseConsumer.js.ejs +30 -0
  29. package/templates/common/kafka/js/messaging/baseConsumer.spec.js.ejs +58 -0
  30. package/templates/common/kafka/js/messaging/userEventSchema.js.ejs +11 -0
  31. package/templates/common/kafka/js/messaging/userEventSchema.spec.js.ejs +27 -0
  32. package/templates/common/kafka/js/messaging/welcomeEmailConsumer.js.ejs +31 -0
  33. package/templates/common/kafka/js/messaging/welcomeEmailConsumer.spec.js.ejs +49 -0
  34. package/templates/common/kafka/js/services/kafkaService.js.ejs +75 -23
  35. package/templates/common/kafka/js/services/kafkaService.spec.js.ejs +53 -7
  36. package/templates/common/kafka/ts/messaging/baseConsumer.spec.ts.ejs +50 -0
  37. package/templates/common/kafka/ts/messaging/baseConsumer.ts.ejs +27 -0
  38. package/templates/common/kafka/ts/messaging/userEventSchema.spec.ts.ejs +51 -0
  39. package/templates/common/kafka/ts/messaging/userEventSchema.ts.ejs +11 -0
  40. package/templates/common/kafka/ts/messaging/welcomeEmailConsumer.spec.ts.ejs +49 -0
  41. package/templates/common/kafka/ts/messaging/welcomeEmailConsumer.ts.ejs +25 -0
  42. package/templates/common/kafka/ts/services/kafkaService.spec.ts.ejs +22 -2
  43. package/templates/common/kafka/ts/services/kafkaService.ts.ejs +72 -12
  44. package/templates/common/package.json.ejs +6 -4
  45. package/templates/common/prompts/add-feature.md.ejs +26 -0
  46. package/templates/common/prompts/project-context.md.ejs +43 -0
  47. package/templates/common/prompts/troubleshoot.md.ejs +28 -0
  48. package/templates/mvc/js/src/controllers/userController.js.ejs +14 -0
  49. package/templates/mvc/js/src/controllers/userController.spec.js.ejs +39 -0
  50. package/templates/mvc/js/src/index.js.ejs +12 -11
  51. package/templates/mvc/ts/src/config/swagger.ts.ejs +1 -1
  52. package/templates/mvc/ts/src/controllers/userController.spec.ts.ejs +18 -0
  53. package/templates/mvc/ts/src/controllers/userController.ts.ejs +16 -0
  54. package/templates/mvc/ts/src/index.ts.ejs +13 -16
  55. package/templates/mvc/ts/src/utils/logger.spec.ts.ejs +0 -1
@@ -4,6 +4,12 @@ const GetAllUsers = require('@/usecases/GetAllUsers');
4
4
 
5
5
  jest.mock('@/usecases/CreateUser');
6
6
  jest.mock('@/usecases/GetAllUsers');
7
+ <%_ if (communication === 'Kafka') { -%>
8
+ jest.mock('@/infrastructure/messaging/kafkaClient', () => ({
9
+ sendMessage: jest.fn().mockResolvedValue(undefined)
10
+ }));
11
+ <%_ } -%>
12
+
7
13
 
8
14
  describe('UserController (Clean Architecture)', () => {
9
15
  let userController;
@@ -81,7 +87,12 @@ describe('UserController (Clean Architecture)', () => {
81
87
  expect(result).toEqual(expectedUser);
82
88
  <%_ } else { -%>
83
89
  await userController.createUser(mockRequest, mockResponse, mockNext);
90
+ <%_ if (communication === 'Kafka') { -%>
91
+ const { sendMessage } = require('@/infrastructure/messaging/kafkaClient');
92
+ expect(sendMessage).toHaveBeenCalled();
93
+ <%_ } -%>
84
94
  expect(mockResponse.status).toHaveBeenCalledWith(201);
95
+
85
96
  expect(mockResponse.json).toHaveBeenCalledWith(expectedUser);
86
97
  <%_ } -%>
87
98
  expect(mockCreateUserUseCase.execute).toHaveBeenCalledWith(payload.name, payload.email);
@@ -98,5 +109,30 @@ describe('UserController (Clean Architecture)', () => {
98
109
  expect(mockNext).toHaveBeenCalledWith(error);
99
110
  <%_ } -%>
100
111
  });
112
+
113
+ <%_ if (communication === 'Kafka') { -%>
114
+ it('should successfully create a new user with _id for Kafka (Happy Path)', async () => {
115
+ const payload = { name: 'Bob', email: 'bob@example.com' };
116
+ <% if (communication === 'GraphQL') { -%>
117
+ const dataArg = payload;
118
+ <% } else { -%>
119
+ mockRequest.body = payload;
120
+ <% } -%>
121
+ const expectedUser = { _id: '2', ...payload };
122
+
123
+ mockCreateUserUseCase.execute.mockResolvedValue(expectedUser);
124
+
125
+ <% if (communication === 'GraphQL') { -%>
126
+ await userController.createUser(dataArg);
127
+ <% } else { -%>
128
+ await userController.createUser(mockRequest, mockResponse, mockNext);
129
+ <% } -%>
130
+ const { sendMessage } = require('@/infrastructure/messaging/kafkaClient');
131
+ expect(sendMessage).toHaveBeenCalledWith(
132
+ 'user-topic',
133
+ expect.stringContaining('"id":"2"')
134
+ );
135
+ });
136
+ <%_ } -%>
101
137
  });
102
138
  });
@@ -1,4 +1,4 @@
1
- <% if (communication === 'REST APIs') { %>import path from 'path';
1
+ <% if (communication === 'REST APIs' || communication === 'Kafka') { %>import path from 'path';
2
2
  import YAML from 'yamljs';
3
3
 
4
4
  const swaggerDocument = YAML.load(path.join(__dirname, 'swagger.yml'));
@@ -8,11 +8,11 @@ import morgan from 'morgan';
8
8
  import { errorMiddleware } from '@/utils/errorMiddleware';
9
9
  import { setupGracefulShutdown } from '@/utils/gracefulShutdown';
10
10
  import healthRoutes from '@/interfaces/routes/healthRoute';
11
- <% if (communication === 'REST APIs') { -%>
11
+ <% if (communication === 'REST APIs' || communication === 'Kafka') { -%>
12
12
  import userRoutes from '@/interfaces/routes/userRoutes';
13
13
  import swaggerUi from 'swagger-ui-express';
14
- import swaggerSpecs from '@/config/swagger';<% } -%>
15
- <%_ if (communication === 'Kafka') { -%>import { KafkaService } from '@/infrastructure/messaging/kafkaClient';<%_ } -%>
14
+ import swaggerSpecs from '@/config/swagger';<% } %>
15
+ <%_ if (communication === 'Kafka') { -%>import { kafkaService } from '@/infrastructure/messaging/kafkaClient';<%_ } -%>
16
16
  <%_ if (communication === 'GraphQL') { -%>
17
17
  import { ApolloServer } from '@apollo/server';
18
18
  import { expressMiddleware } from '@apollo/server/express4';
@@ -52,10 +52,10 @@ app.use(limiter);
52
52
  app.use(express.json());
53
53
  app.use(morgan('combined', { stream: { write: (message) => logger.info(message.trim()) } }));
54
54
 
55
- <%_ if (communication === 'REST APIs') { -%>
55
+ <%_ if (communication === 'REST APIs' || communication === 'Kafka') { -%>
56
56
  app.use('/api/users', userRoutes);
57
57
  <%_ } -%>
58
- <%_ if (communication === 'REST APIs') { -%>
58
+ <%_ if (communication === 'REST APIs' || communication === 'Kafka') { -%>
59
59
  app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpecs));
60
60
  <%_ } -%>
61
61
  app.use('/health', healthRoutes);
@@ -92,18 +92,16 @@ const startServer = async () => {
92
92
  app.use('/graphql', expressMiddleware(apolloServer, { context: gqlContext }));
93
93
  <%_ } -%>
94
94
  app.use(errorMiddleware);
95
- <%_ if (communication === 'Kafka') { -%>
96
- const kafkaService = new KafkaService();
97
- <%_ } -%>
98
95
  const server = app.listen(port, () => {
99
96
  logger.info(`Server running on port ${port}`);
100
97
  <%_ if (communication === 'Kafka') { -%>
101
- kafkaService.connect().then(() => {
102
- logger.info('Kafka connected');
103
- kafkaService.sendMessage('test-topic', 'Hello Kafka from Clean Arch TS!');
104
- }).catch(err => {
105
- logger.error('Failed to connect to Kafka:', err);
106
- });
98
+ kafkaService.connect()
99
+ .then(async () => {
100
+ logger.info('Kafka connected');
101
+ })
102
+ .catch(err => {
103
+ logger.error('Failed to connect to Kafka after retries:', (err as Error).message);
104
+ });
107
105
  <%_ } -%>
108
106
  });
109
107
 
@@ -56,7 +56,6 @@ describe('Logger', () => {
56
56
  const winston = require('winston');
57
57
  jest.resetModules();
58
58
  process.env.NODE_ENV = 'production';
59
- // eslint-disable-next-line @typescript-eslint/no-require-imports
60
59
  require('<% if (architecture === "MVC") { %>@/utils/logger<% } else { %>@/infrastructure/log/logger<% } %>');
61
60
  expect(winston.format.json).toHaveBeenCalled();
62
61
  process.env.NODE_ENV = 'test';
@@ -11,6 +11,20 @@ jest.mock('@/infrastructure/repositories/UserRepository');
11
11
  jest.mock('@/usecases/createUser');
12
12
  jest.mock('@/usecases/getAllUsers');
13
13
  jest.mock('@/infrastructure/log/logger');
14
+ <%_ if (communication === 'Kafka') { -%>
15
+ jest.mock('@/infrastructure/messaging/kafkaClient', () => {
16
+ const mockSendMessage = jest.fn().mockResolvedValue(undefined);
17
+ return {
18
+ kafkaService: {
19
+ sendMessage: mockSendMessage
20
+ },
21
+ KafkaService: jest.fn().mockImplementation(() => ({
22
+ sendMessage: mockSendMessage
23
+ }))
24
+ };
25
+ });
26
+ <%_ } -%>
27
+
14
28
 
15
29
  describe('UserController (Clean Architecture)', () => {
16
30
  let userController: UserController;
@@ -115,7 +129,12 @@ describe('UserController (Clean Architecture)', () => {
115
129
  await userController.createUser(mockRequest as Request, mockResponse as Response, mockNext);
116
130
 
117
131
  // Assert
132
+ <%_ if (communication === 'Kafka') { -%>
133
+ const { kafkaService } = require('@/infrastructure/messaging/kafkaClient');
134
+ expect(kafkaService.sendMessage).toHaveBeenCalled();
135
+ <%_ } -%>
118
136
  expect(mockResponse.status).toHaveBeenCalledWith(HTTP_STATUS.CREATED);
137
+
119
138
  expect(mockResponse.json).toHaveBeenCalledWith(expectedUser);
120
139
  <% } -%>
121
140
  expect(mockCreateUserUseCase.execute).toHaveBeenCalledWith(payload.name, payload.email);
@@ -22,6 +22,14 @@ export class UserController {
22
22
  try {
23
23
  const { name, email } = data;
24
24
  const user = await this.createUserUseCase.execute(name, email);
25
+ <%_ if (communication === 'Kafka') { -%>
26
+ const { kafkaService } = await import('@/infrastructure/messaging/kafkaClient');
27
+ await kafkaService.sendMessage('user-topic', JSON.stringify({
28
+ action: 'USER_CREATED',
29
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
30
+ payload: { id: (user as any).id || (user as any)._id, email: user.email }
31
+ }));
32
+ <%_ } -%>
25
33
  return user;
26
34
  } catch (error: unknown) {
27
35
  const message = error instanceof Error ? error.message : 'Unknown error';
@@ -45,6 +53,14 @@ export class UserController {
45
53
  try {
46
54
  const { name, email } = req.body;
47
55
  const user = await this.createUserUseCase.execute(name, email);
56
+ <%_ if (communication === 'Kafka') { -%>
57
+ const { kafkaService } = await import('@/infrastructure/messaging/kafkaClient');
58
+ await kafkaService.sendMessage('user-topic', JSON.stringify({
59
+ action: 'USER_CREATED',
60
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
61
+ payload: { id: (user as any).id || (user as any)._id, email: user.email }
62
+ }));
63
+ <%_ } -%>
48
64
  res.status(HTTP_STATUS.CREATED).json(user);
49
65
  } catch (error: unknown) {
50
66
  const message = error instanceof Error ? error.message : 'Unknown error';
@@ -0,0 +1,60 @@
1
+ # Cursor AI Coding Rules for <%= projectName %>
2
+
3
+ ## Project Context
4
+ You are an expert working on **<%= projectName %>**.
5
+ - **Project Goal**: [Replace this with your business logic, e.g., E-commerce API]
6
+ - **Language**: <%= language %>
7
+ - **Architecture**: <%= architecture %>
8
+ - **Database**: <%= database %>
9
+ - **Communication**: <%= communication %>
10
+
11
+ ## Excluded Files/Folders
12
+ When indexing or searching the workspace, ignore the following paths to prevent context pollution:
13
+ - `node_modules/`
14
+ - `dist/`
15
+ - `build/`
16
+ - `coverage/`
17
+ - `.git/`
18
+
19
+ ## Strict Rules
20
+
21
+ ### 1. Testing First
22
+ - Every new service or controller method MUST have a test file in `tests/`.
23
+ - **Coverage Gate**: Aim for > 70% coverage (Statement/Line/Function/Branch).
24
+ - **Format**: Use Jest with the AAA (Arrange, Act, Assert) pattern.
25
+ - **Isolation**: Mock external dependencies (DB, Redis, etc.) using `jest.mock()`.
26
+
27
+ ### 2. Error Handling
28
+ - Do NOT use generic `Error`.
29
+ - Use custom classes from `src/errors/` (e.g., `ApiError`, `NotFoundError`, `BadRequestError`).
30
+ <% if (language === 'TypeScript') { -%>
31
+ - Use `HTTP_STATUS` constants from `@/utils/httpCodes` for status codes.
32
+ <% } else { -%>
33
+ - Use `HTTP_STATUS` constants from `../utils/httpCodes.js` for status codes.
34
+ <% } -%>
35
+
36
+ ### 3. File Naming & Style
37
+ - **Controllers**: camelCase (e.g., `userController.<% if (language === 'TypeScript') { %>ts<% } else { %>js<% } %>`).
38
+ - **Services**: camelCase (e.g., `userService.<% if (language === 'TypeScript') { %>ts<% } else { %>js<% } %>`).
39
+ - **Routes**: camelCase (e.g., `userRoutes.<% if (language === 'TypeScript') { %>ts<% } else { %>js<% } %>`).
40
+ <% if (language === 'TypeScript') { -%>
41
+ - **Imports**: Use path aliases (e.g., `@/services/...`) instead of relative paths.
42
+ - **Typing**: Ensure strong typing for interfaces and DTOs. Do not use `any` unless absolutely necessary.
43
+ <% } else { -%>
44
+ - **Imports**: Use relative paths as dictated by the directory structure.
45
+ <% } -%>
46
+
47
+ ### 4. Architecture Standards
48
+ <% if (architecture === 'Clean Architecture') { -%>
49
+ - Enforce strict separation of concerns:
50
+ - `domain`: Entities and enterprise business rules.
51
+ - `usecases`: Application business rules.
52
+ - `interfaces`: Controllers and Routes.
53
+ - `infrastructure`: Frameworks, Database, Caching, and Web Server.
54
+ - Dependencies point inward toward the `domain`.
55
+ <% } else { -%>
56
+ - Enforce MVC standards:
57
+ - `models`: Data layer.
58
+ - `controllers`: Request handlers and business logic.
59
+ - `routes`: Define endpoints routing to controllers.
60
+ <% } -%>
@@ -8,3 +8,5 @@ README.md
8
8
  docker-compose.yml
9
9
  test_results.log
10
10
  flyway/sql
11
+ .cursorrules
12
+ prompts
@@ -12,24 +12,24 @@ cache:
12
12
 
13
13
  install_dependencies:
14
14
  stage: .pre
15
- image: node:18-alpine
15
+ image: node:22-alpine
16
16
  script:
17
17
  - npm ci
18
18
 
19
19
  lint_code:
20
20
  stage: lint
21
- image: node:18-alpine
21
+ image: node:22-alpine
22
22
  script:
23
23
  - npm run lint
24
24
 
25
25
  run_tests:
26
26
  stage: test
27
- image: node:18-alpine
27
+ image: node:22-alpine
28
28
  script:
29
- - npm test
29
+ - npm run test:coverage
30
30
 
31
31
  build_app:
32
32
  stage: build
33
- image: node:18-alpine
33
+ image: node:22-alpine
34
34
  script:
35
35
  - npm run build --if-present
@@ -4,6 +4,7 @@
4
4
  FROM node:22-alpine AS builder
5
5
 
6
6
  WORKDIR /app
7
+ ENV NPM_CONFIG_UPDATE_NOTIFIER=false
7
8
 
8
9
  COPY package*.json ./
9
10
  COPY tsconfig*.json ./
@@ -24,6 +25,7 @@ FROM node:22-alpine AS production
24
25
  WORKDIR /app
25
26
 
26
27
  ENV NODE_ENV=production
28
+ ENV NPM_CONFIG_UPDATE_NOTIFIER=false
27
29
 
28
30
  COPY package*.json ./
29
31
 
@@ -21,7 +21,7 @@ pipeline {
21
21
 
22
22
  stage('Test') {
23
23
  steps {
24
- sh 'npm test'
24
+ sh 'npm run test:coverage'
25
25
  }
26
26
  }
27
27
 
@@ -115,6 +115,29 @@ mutation CreateUser {
115
115
  API is exposed via **REST**.
116
116
  A Swagger UI for API documentation is available at:
117
117
  - **URL**: `http://localhost:3000/api-docs` (Dynamic based on PORT)
118
+ <%_ } -%>
119
+
120
+ <% if (communication === 'Kafka') { -%>
121
+ ## 📡 Testing Kafka Asynchronous Flow
122
+ This project demonstrates a production-ready Kafka flow:
123
+ 1. **Producer**: When a user is created via the API, a `USER_CREATED` event is sent to `user-topic`.
124
+ 2. **Consumer**: `WelcomeEmailConsumer` listens to `user-topic` and simulates sending an email.
125
+
126
+ ### How to verify:
127
+ 1. Ensure infrastructure is running: `docker-compose up -d<% if (database !== 'None') { %> db<% } %><% if (caching === 'Redis') { %> redis<% } %><% if (communication === 'Kafka') { %> zookeeper kafka<% } %>`
128
+ 2. Start the app: `npm run dev`
129
+ 3. Trigger an event by creating a user (via Postman or curl):
130
+ ```bash
131
+ curl -X POST http://localhost:3000/api/users \
132
+ -H "Content-Type: application/json" \
133
+ -d '{"name": "Kafka Tester", "email": "kafka@example.com"}'
134
+ ```
135
+ 4. Observe the logs:
136
+ ```text
137
+ [Kafka] Producer: Sent USER_CREATED event for 'kafka@example.com'
138
+ [Kafka] Consumer: Received USER_CREATED.
139
+ [Kafka] Consumer: 📧 Sending welcome email to 'kafka@example.com'... Done!
140
+ ```
118
141
  <% } -%>
119
142
 
120
143
  <% if (caching === 'Redis') { -%>
@@ -217,4 +240,14 @@ docker-compose down
217
240
  - **Helmet**: Sets secure HTTP headers.
218
241
  - **CORS**: Configured for cross-origin requests.
219
242
  - **Rate Limiting**: Protects against DDoS / Brute-force.
220
- - **HPP**: Prevents HTTP Parameter Pollution attacks.
243
+ - **HPP**: Prevents HTTP Parameter Pollution attacks.
244
+
245
+
246
+ ## 🤖 AI-Native Development
247
+
248
+ This project is "AI-Ready" out of the box. We have pre-configured industry-leading AI context files to bridge the gap between "Generated Code" and "AI-Assisted Development."
249
+
250
+ - **Magic Defaults**: We've automatically tailored your AI context to focus on **<%= projectName %>** and its specific architectural stack (<%= architecture %>, <%= database %>, etc.).
251
+ - **Use Cursor?** We've configured **`.cursorrules`** at the root. It enforces project standards (70% coverage, MVC/Clean) directly within the editor.
252
+ - *Pro-tip*: You can customize the `Project Goal` placeholder in `.cursorrules` to help the AI understand your specific business logic!
253
+ - **Use ChatGPT/Gemini/Claude?** Check the **`prompts/`** directory. It contains highly-specialized Agent Skill templates. You can copy-paste these into any LLM to give it a "Senior Developer" understanding of your codebase immediately.
@@ -1,5 +1,8 @@
1
1
  name: Node.js CI
2
2
 
3
+ env:
4
+ FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true'
5
+
3
6
  on:
4
7
  push:
5
8
  branches: [ "main" ]
@@ -13,13 +16,13 @@ jobs:
13
16
 
14
17
  strategy:
15
18
  matrix:
16
- node-version: [18.x, 20.x]
19
+ node-version: [20.x, 22.x]
17
20
 
18
21
  steps:
19
- - uses: actions/checkout@v3
22
+ - uses: actions/checkout@v4
20
23
 
21
24
  - name: Use Node.js ${{ matrix.node-version }}
22
- uses: actions/setup-node@v3
25
+ uses: actions/setup-node@v4
23
26
  with:
24
27
  node-version: ${{ matrix.node-version }}
25
28
  cache: 'npm'
@@ -31,7 +34,7 @@ jobs:
31
34
  run: npm run lint
32
35
 
33
36
  - name: Run Tests
34
- run: npm test
37
+ run: npm run test:coverage
35
38
 
36
39
  - name: Build
37
40
  run: npm run build --if-present
@@ -11,7 +11,8 @@ class UserModel {
11
11
  }
12
12
 
13
13
  static async create(data) {
14
- const newUser = { id: String(this.mockData.length + 1), ...data };
14
+ const { id, ...rest } = data;
15
+ const newUser = { id: String(this.mockData.length + 1), ...rest };
15
16
  this.mockData.push(newUser);
16
17
  return newUser;
17
18
  }
@@ -16,10 +16,11 @@ export default class UserModel {
16
16
  return this.mockData;
17
17
  }
18
18
 
19
- static async create(data: Omit<User, 'id'>) {
20
- const newUser = { id: String(this.mockData.length + 1), ...data };
19
+ static async create(data: Omit<User, 'id'> & { id?: string | number | null }) {
20
+ const { id, ...rest } = data;
21
+ const newUser: User = { id: String(this.mockData.length + 1), ...rest };
21
22
  this.mockData.push(newUser);
22
- return newUser as User;
23
+ return newUser;
23
24
  }
24
25
  }
25
26
  <% } else { -%>
@@ -1,6 +1,7 @@
1
1
  import eslint from '@eslint/js';
2
2
  import eslintConfigPrettier from 'eslint-config-prettier';
3
3
  import globals from 'globals';
4
+ import importPlugin from 'eslint-plugin-import-x';
4
5
  <% if (language === 'TypeScript') { %>import tseslint from 'typescript-eslint';
5
6
 
6
7
  export default tseslint.config(
@@ -8,6 +9,15 @@ export default tseslint.config(
8
9
  ...tseslint.configs.recommended,
9
10
  eslintConfigPrettier,
10
11
  {
12
+ plugins: {
13
+ import: importPlugin,
14
+ },
15
+ settings: {
16
+ 'import-x/resolver': {
17
+ typescript: true,
18
+ node: true,
19
+ },
20
+ },
11
21
  languageOptions: {
12
22
  globals: {
13
23
  ...globals.node,
@@ -17,16 +27,24 @@ export default tseslint.config(
17
27
  rules: {
18
28
  "no-console": "warn",
19
29
  "no-unused-vars": "off",
20
- "@typescript-eslint/no-unused-vars": "warn",
30
+ "@typescript-eslint/no-unused-vars": ["warn", {
31
+ "argsIgnorePattern": "^_",
32
+ "varsIgnorePattern": "^_"
33
+ }],
21
34
  "@typescript-eslint/no-require-imports": "error",
35
+ "import/no-unresolved": [2, { "caseSensitive": false }]
22
36
  },
23
37
  },
24
38
  {
25
39
  files: ["**/*.test.ts", "**/*.spec.ts", "tests/**/*.ts"],
26
40
  rules: {
27
41
  "@typescript-eslint/no-require-imports": "off",
28
- "@typescript-eslint/no-unused-vars": "warn",
42
+ "@typescript-eslint/no-unused-vars": ["warn", {
43
+ "argsIgnorePattern": "^_",
44
+ "varsIgnorePattern": "^_"
45
+ }],
29
46
  "@typescript-eslint/no-explicit-any": "off",
47
+ "import/no-unresolved": [2, { "caseSensitive": false }]
30
48
  },
31
49
  }
32
50
  );<% } else { %>
@@ -34,6 +52,14 @@ export default [
34
52
  eslint.configs.recommended,
35
53
  eslintConfigPrettier,
36
54
  {
55
+ plugins: {
56
+ import: importPlugin,
57
+ },
58
+ settings: {
59
+ 'import-x/resolver': {
60
+ node: true,
61
+ },
62
+ },
37
63
  languageOptions: {
38
64
  ecmaVersion: 'latest',
39
65
  sourceType: 'module',
@@ -44,7 +70,8 @@ export default [
44
70
  },
45
71
  rules: {
46
72
  "no-console": "warn",
47
- "no-unused-vars": "warn"
73
+ "no-unused-vars": "warn",
74
+ "import/no-unresolved": [2, { "caseSensitive": false }]
48
75
  }
49
76
  }
50
77
  ];<% } %>
@@ -13,9 +13,12 @@ module.exports = {
13
13
  "src/index",
14
14
  "src/app",
15
15
  "src/config/env",
16
+ "src/infrastructure/config/env",
16
17
  "src/config/swagger",
17
18
  "src/infrastructure/webserver/swagger",
18
- "src/infrastructure/webserver/server"
19
+ "src/infrastructure/webserver/server",
20
+ "src/utils/logger",
21
+ "src/infrastructure/log/logger"
19
22
  ],
20
23
  coverageThreshold: {
21
24
  global: {
@@ -0,0 +1,30 @@
1
+ const logger = require('<% if (architecture === "Clean Architecture") { %>../../infrastructure/log/logger<% } else { %>../utils/logger<% } %>');
2
+
3
+ class BaseConsumer {
4
+ constructor() {
5
+ if (this.constructor === BaseConsumer) {
6
+ throw new Error("Abstract class 'BaseConsumer' cannot be instantiated.");
7
+ }
8
+ }
9
+
10
+ get topic() { throw new Error("Property 'topic' must be implemented"); }
11
+ get groupId() { throw new Error("Property 'groupId' must be implemented"); }
12
+
13
+ async onMessage({ message }) {
14
+ try {
15
+ const rawValue = message.value?.toString();
16
+ if (!rawValue) return;
17
+
18
+ const data = JSON.parse(rawValue);
19
+ await this.handle(data);
20
+ } catch (error) {
21
+ logger.error(`[Kafka] Error processing message on topic ${this.topic}:`, error);
22
+ }
23
+ }
24
+
25
+ async handle(data) {
26
+ throw new Error("Method 'handle()' must be implemented");
27
+ }
28
+ }
29
+
30
+ module.exports = BaseConsumer;
@@ -0,0 +1,58 @@
1
+ const BaseConsumer = require('<% if (architecture === "Clean Architecture") { %>@/interfaces/messaging/baseConsumer<% } else { %>@/messaging/baseConsumer<% } %>');
2
+ const logger = require('<% if (architecture === "Clean Architecture") { %>@/infrastructure/log/logger<% } else { %>@/utils/logger<% } %>');
3
+
4
+ jest.mock('<% if (architecture === "Clean Architecture") { %>@/infrastructure/log/logger<% } else { %>@/utils/logger<% } %>');
5
+
6
+ class TestConsumer extends BaseConsumer {
7
+ constructor() {
8
+ super();
9
+ this._topic = 'test-topic';
10
+ }
11
+ get topic() { return this._topic; }
12
+ get groupId() { return 'test-group'; }
13
+ async handle(data) {
14
+ logger.info('Handled', data);
15
+ }
16
+ }
17
+
18
+ describe('BaseConsumer', () => {
19
+ let consumer;
20
+
21
+ beforeEach(() => {
22
+ jest.clearAllMocks();
23
+ consumer = new TestConsumer();
24
+ });
25
+
26
+ it('should throw error if instantiated directly', () => {
27
+ expect(() => new BaseConsumer()).toThrow("Abstract class 'BaseConsumer' cannot be instantiated.");
28
+ });
29
+
30
+ it('should process a valid message', async () => {
31
+ const message = { value: Buffer.from(JSON.stringify({ test: 'data' })) };
32
+ await consumer.onMessage({ message });
33
+ expect(logger.info).toHaveBeenCalledWith('Handled', { test: 'data' });
34
+ });
35
+
36
+ it('should handle invalid JSON', async () => {
37
+ const message = { value: Buffer.from('invalid-json') };
38
+ await consumer.onMessage({ message });
39
+ expect(logger.error).toHaveBeenCalledWith(
40
+ expect.stringContaining('[Kafka] Error processing message on topic test-topic:'),
41
+ expect.anything()
42
+ );
43
+ });
44
+
45
+ it('should skip empty messages', async () => {
46
+ const message = { value: null };
47
+ await consumer.onMessage({ message });
48
+ expect(logger.info).not.toHaveBeenCalled();
49
+ });
50
+
51
+ it('should throw if topic, groupId or handle are not implemented', async () => {
52
+ class IncompleteConsumer extends BaseConsumer {}
53
+ const incomplete = new IncompleteConsumer();
54
+ expect(() => incomplete.topic).toThrow("Property 'topic' must be implemented");
55
+ expect(() => incomplete.groupId).toThrow("Property 'groupId' must be implemented");
56
+ await expect(incomplete.handle({})).rejects.toThrow("Method 'handle()' must be implemented");
57
+ });
58
+ });
@@ -0,0 +1,11 @@
1
+ const { z } = require('zod');
2
+
3
+ const UserEventSchema = z.object({
4
+ action: z.enum(['USER_CREATED', 'UPDATE_USER', 'DELETE_USER']),
5
+ payload: z.object({
6
+ id: z.union([z.string(), z.number()]),
7
+ email: z.string().email(),
8
+ }),
9
+ });
10
+
11
+ module.exports = { UserEventSchema };
@@ -0,0 +1,27 @@
1
+ const { UserEventSchema } = require('<% if (architecture === "Clean Architecture") { %>@/interfaces/messaging/schemas/userEventSchema<% } else { %>@/messaging/schemas/userEventSchema<% } %>');
2
+
3
+ describe('UserEventSchema', () => {
4
+ it('should validate a correct USER_CREATED event', () => {
5
+ const validEvent = {
6
+ action: 'USER_CREATED',
7
+ payload: {
8
+ id: 1,
9
+ email: 'test@example.com'
10
+ }
11
+ };
12
+ const result = UserEventSchema.safeParse(validEvent);
13
+ expect(result.success).toBe(true);
14
+ });
15
+
16
+ it('should fail validation for invalid email', () => {
17
+ const invalidEvent = {
18
+ action: 'USER_CREATED',
19
+ payload: {
20
+ id: 1,
21
+ email: 'invalid-email'
22
+ }
23
+ };
24
+ const result = UserEventSchema.safeParse(invalidEvent);
25
+ expect(result.success).toBe(false);
26
+ });
27
+ });