nodejs-quickstart-structure 1.10.1 → 1.11.1

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 (42) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +4 -0
  3. package/docs/generateCase.md +245 -165
  4. package/docs/generatorFlow.md +65 -23
  5. package/lib/generator.js +4 -1
  6. package/lib/modules/app-setup.js +56 -5
  7. package/package.json +1 -1
  8. package/templates/clean-architecture/js/src/errors/ApiError.js +14 -0
  9. package/templates/clean-architecture/js/src/errors/BadRequestError.js +10 -0
  10. package/templates/clean-architecture/js/src/errors/NotFoundError.js +10 -0
  11. package/templates/clean-architecture/js/src/infrastructure/webserver/middlewares/error.middleware.js +29 -0
  12. package/templates/clean-architecture/js/src/infrastructure/webserver/server.js.ejs +24 -0
  13. package/templates/clean-architecture/js/src/interfaces/controllers/userController.js.ejs +4 -7
  14. package/templates/clean-architecture/js/src/interfaces/graphql/resolvers/user.resolvers.js.ejs +2 -11
  15. package/templates/clean-architecture/js/src/interfaces/routes/api.js +2 -2
  16. package/templates/clean-architecture/ts/src/errors/ApiError.ts +15 -0
  17. package/templates/clean-architecture/ts/src/errors/BadRequestError.ts +8 -0
  18. package/templates/clean-architecture/ts/src/errors/NotFoundError.ts +8 -0
  19. package/templates/clean-architecture/ts/src/index.ts.ejs +23 -0
  20. package/templates/clean-architecture/ts/src/interfaces/controllers/userController.ts.ejs +13 -19
  21. package/templates/clean-architecture/ts/src/interfaces/graphql/resolvers/user.resolvers.ts.ejs +4 -13
  22. package/templates/clean-architecture/ts/src/interfaces/routes/userRoutes.ts +4 -3
  23. package/templates/clean-architecture/ts/src/utils/error.middleware.ts.ejs +27 -0
  24. package/templates/common/package.json.ejs +5 -6
  25. package/templates/mvc/js/src/controllers/userController.js.ejs +5 -4
  26. package/templates/mvc/js/src/errors/ApiError.js +14 -0
  27. package/templates/mvc/js/src/errors/BadRequestError.js +10 -0
  28. package/templates/mvc/js/src/errors/NotFoundError.js +10 -0
  29. package/templates/mvc/js/src/graphql/resolvers/user.resolvers.js.ejs +2 -11
  30. package/templates/mvc/js/src/index.js.ejs +23 -0
  31. package/templates/mvc/js/src/utils/error.middleware.js +28 -0
  32. package/templates/mvc/ts/src/controllers/userController.ts.ejs +6 -14
  33. package/templates/mvc/ts/src/errors/ApiError.ts +15 -0
  34. package/templates/mvc/ts/src/errors/BadRequestError.ts +8 -0
  35. package/templates/mvc/ts/src/errors/NotFoundError.ts +8 -0
  36. package/templates/mvc/ts/src/graphql/resolvers/user.resolvers.ts.ejs +4 -13
  37. package/templates/mvc/ts/src/index.ts.ejs +23 -0
  38. package/templates/mvc/ts/src/routes/api.ts +3 -3
  39. package/templates/mvc/ts/src/utils/error.middleware.ts.ejs +27 -0
  40. /package/templates/clean-architecture/js/src/infrastructure/webserver/{swagger.js → swagger.js.ejs} +0 -0
  41. /package/templates/clean-architecture/ts/src/infrastructure/repositories/{userRepository.ts.ejs → UserRepository.ts.ejs} +0 -0
  42. /package/templates/mvc/js/src/config/{swagger.js → swagger.js.ejs} +0 -0
@@ -25,7 +25,7 @@ The generator prompts the user for the following configurations. These determine
25
25
  | **View Engine** | `None`, `EJS`, `Pug` | `None` | (MVC Only) Template engine for server-side rendering. |
26
26
  | **Database** | `None`, `MySQL`, `PostgreSQL`, `MongoDB` | `None` | The primary database. |
27
27
  | **Database Name** | Input String | `demo` | The name of the database to use/create. |
28
- | **Communication**| `REST APIs`, `Kafka` | `REST APIs` | The primary communication method. |
28
+ | **Communication**| `REST APIs`, `GraphQL`, `Kafka` | `REST APIs` | The primary communication method. |
29
29
  | **Caching Layer**| `None`, `Redis`, `Memory Cache` | `None` | (If DB selected) Caching solution. |
30
30
  | **CI/CD Provider**| `None`, `GitHub Actions`, `Jenkins`| `None` | Setup for Continuous Integration/Deployment. |
31
31
 
@@ -47,13 +47,23 @@ The `generateProject` function in `lib/generator.js` executes the following step
47
47
  5. **Render `README.md`**:
48
48
  * Generates custom documentation specific to the selected stack.
49
49
  6. **Render `src/index.{js|ts}`**:
50
- * Processes the entry point file to wire up the selected DB and Architecture.
51
- 7. **Dynamic Component Generation**:
52
- * **MVC**: Generates `userController` (imports specific DB service).
50
+ * Processes the entry point file to wire up the selected DB, Architecture, and Communication type.
51
+ * **GraphQL**: Wires up Apollo Server middleware with `formatError` hook for centralized error handling.
52
+ * **REST APIs / Kafka**: Registers `app.use(errorMiddleware)` at the end of the Express chain.
53
+ 7. **Render Error Middleware** (`renderErrorMiddleware`):
54
+ * Processes `error.middleware.{ts|js}.ejs` template from the target directory's `src/utils/` path.
55
+ * Renders to `src/utils/error.middleware.{ts|js}` in the generated project.
56
+ * **Clean Architecture**: Also handles `src/infrastructure/webserver/middlewares/error.middleware.{js}` path.
57
+ 8. **Dynamic Component Generation**:
58
+ * **MVC**: Generates `userController` (imports specific DB service, uses `next(error)`).
53
59
  * **Clean Architecture**: Generates `UserRepository` (infrastructure layer implementation).
54
60
  * **Clean Architecture (JS only)**: Generates `server.js` (webserver setup).
55
- 8. **Communication Setup (Kafka)**:
56
- * If **Kafka** is selected:
61
+ 9. **Communication Setup**:
62
+ * **GraphQL**:
63
+ * Generates Apollo Server v4 schema (`typeDefs`) and resolvers.
64
+ * Configures `formatError` hook with `unwrapResolverError` for structured error mapping.
65
+ * Automatically embeds Apollo Sandbox with local CSP headers (no CDN dependency).
66
+ * **Kafka**:
57
67
  * Copies Kafka client/service templates.
58
68
  * **Clean Architecture Restructuring**:
59
69
  * Moves service to `src/infrastructure/messaging`.
@@ -61,14 +71,14 @@ The `generateProject` function in `lib/generator.js` executes the following step
61
71
  * Removes REST-specific folders (`interfaces/routes`, `interfaces/controllers`).
62
72
  * **MVC Cleanup**:
63
73
  * If no View Engine is selected, removes `src/controllers` and `src/routes` (assumes pure worker).
64
- 9. **Common Configuration**:
74
+ 10. **Common Configuration**:
65
75
  * Copies `.gitignore`, `.dockerignore`, `Dockerfile`.
66
76
  * Copies `tsconfig.json` (if TypeScript).
67
- 10. **Database Setup**:
77
+ 11. **Database Setup**:
68
78
  * **MongoDB**: Sets up `migrate-mongo-config.js` and initial migration script.
69
79
  * **SQL (MySQL/Postgres)**: Sets up `flyway/sql` directory and copies initial SQL migration files.
70
80
  * **None**: Skips migration setup.
71
- 11. **Caching Setup**:
81
+ 12. **Caching Setup**:
72
82
  * **Redis**:
73
83
  * Injects `ioredis` dependency into `package.json`.
74
84
  * Generates `redisClient.{js|ts}` config.
@@ -78,22 +88,23 @@ The `generateProject` function in `lib/generator.js` executes the following step
78
88
  * Injects `node-cache` dependency.
79
89
  * Generates `memoryCache.{js|ts}` config.
80
90
  * **MVC/Clean**: Consumes the generic abstraction injected above.
81
- 12. **Database Connection Config**:
91
+ 13. **Database Connection Config**:
82
92
  * Renders `database.{js|ts}` or `mongoose.{js|ts}` based on DB selection.
83
93
  * Places it in `src/config` (MVC) or `src/infrastructure/database` (Clean Arch).
84
94
  * **None**: Skips this step.
85
- 12. **Model Generation**:
95
+ 14. **Model Generation**:
86
96
  * Renders `User` model (Mongoose schema or Sequelize/TypeORM model) in the appropriate directory.
87
97
  * **None**: Generates a simple Mock Entity/Model class with in-memory data for testing.
88
- 13. **View Engine Setup (MVC)**:
98
+ 15. **View Engine Setup (MVC)**:
89
99
  * If selected, copies views (`views/ejs` or `views/pug`) and `public` assets.
90
- 14. **Swagger Config**:
91
- * If **REST APIs** is selected, generates Swagger configuration.
92
- 15. **Code Quality Setup**:
100
+ 16. **Swagger Config** (`renderSwaggerConfig`):
101
+ * If **REST APIs** is selected, generates `swagger.yml` and `swagger.{ts|js}` config.
102
+ * Cleans up `.ejs` template copies for non-REST configs (GraphQL, Kafka).
103
+ 17. **Code Quality Setup**:
93
104
  * Generates `.eslintrc.json`, `.prettierrc`, `.lintstagedrc`.
94
- 16. **Test Setup**:
105
+ 18. **Test Setup**:
95
106
  * Generates `jest.config.js` and a sample `health.test.{js|ts}`.
96
- 17. **CI/CD Setup**:
107
+ 19. **CI/CD Setup**:
97
108
  * Helper: `setupCiCd`
98
109
  * Checks `config.ciProvider`:
99
110
  * **GitHub Actions**: Copies `.github/workflows/ci.yml`.
@@ -125,16 +136,41 @@ Standard architecture for web APIs.
125
136
  project-name/
126
137
  ├── src/
127
138
  │ ├── config/ # Database, Redis, Swagger, etc.
128
- │ ├── controllers/ # Request handlers
139
+ │ ├── controllers/ # Request handlers (use next(error))
140
+ │ ├── errors/ # ApiError, NotFoundError, BadRequestError
129
141
  │ ├── models/ # Database models
130
142
  │ ├── routes/ # Express routes
131
- └── index.js|ts # Entry point
143
+ ├── utils/
144
+ │ │ ├── error.middleware.{ts|js} # Global error handler
145
+ │ │ ├── logger.{ts|js}
146
+ │ │ └── httpCodes.{ts|js}
147
+ │ └── index.js|ts # Entry point (registers errorMiddleware last)
132
148
  ├── tests/ # Jest tests
133
149
  ├── package.json
134
150
  ├── Dockerfile
135
151
  └── docker-compose.yml
136
152
  ```
137
153
 
154
+ ### Case A2: MVC (GraphQL)
155
+ Apollo Server v4 mounted as Express middleware.
156
+
157
+ ```text
158
+ project-name/
159
+ ├── src/
160
+ │ ├── config/ # Database, Redis config
161
+ │ ├── controllers/ # GraphQL resolver backing logic (throws errors)
162
+ │ ├── errors/ # ApiError, NotFoundError, BadRequestError
163
+ │ ├── graphql/
164
+ │ │ ├── schema/ # typeDefs
165
+ │ │ └── resolvers/ # user.resolvers (calls controllers, throws errors)
166
+ │ ├── models/
167
+ │ ├── utils/
168
+ │ │ ├── error.middleware.{ts|js} # Express-level fallback error handler
169
+ │ │ └── logger.{ts|js}
170
+ │ └── index.js|ts # Apollo Server + formatError hook
171
+ └── ...
172
+ ```
173
+
138
174
  ### Case B: MVC (Web App with Views)
139
175
  Includes frontend views rendered on the server.
140
176
 
@@ -158,17 +194,23 @@ Separation of concerns with Domain, Use Cases, and Infrastructure.
158
194
  project-name/
159
195
  ├── src/
160
196
  │ ├── domain/ # Entities (Enterprise rules)
197
+ │ ├── errors/ # ApiError, NotFoundError, BadRequestError
161
198
  │ ├── use_cases/ # Application business rules
162
199
  │ ├── interfaces/ # Adapters
163
- │ │ ├── controllers/
164
- │ │ └── routes/
200
+ │ │ ├── controllers/ # Use next(error) for error propagation
201
+ │ │ └── routes/ # Pass NextFunction through handlers
165
202
  │ ├── infrastructure/ # Frameworks & Drivers
166
203
  │ │ ├── config/ # Environment config
167
204
  │ │ ├── caching/ # Redis Client
168
205
  │ │ ├── database/ # DB connection & models
169
206
  │ │ ├── repositories/ # Data access implementation
170
- │ │ └── webserver/ # Express server setup
171
- └── index.js|ts
207
+ │ │ └── webserver/
208
+ │ ├── middlewares/
209
+ │ │ │ └── error.middleware.js # JS only — Express error handler
210
+ │ │ └── server.js # Express app setup
211
+ │ ├── utils/
212
+ │ │ └── error.middleware.ts # TS — global error handler
213
+ │ └── index.js|ts # Registers errorMiddleware after Apollo/Express
172
214
  └── ...
173
215
  ```
174
216
 
package/lib/generator.js CHANGED
@@ -2,7 +2,7 @@ import path from 'path';
2
2
  import { fileURLToPath } from 'url';
3
3
  import { setupProjectDirectory, copyBaseStructure, copyCommonFiles } from './modules/project-setup.js';
4
4
  import { renderPackageJson, renderDockerCompose, renderReadme, renderDockerfile, renderProfessionalConfig, setupCiCd, renderTestSample, renderEnvExample } from './modules/config-files.js';
5
- import { renderIndexFile, renderDynamicComponents, renderSwaggerConfig, setupViews as setupSrcViews } from './modules/app-setup.js';
5
+ import { renderIndexFile, renderErrorMiddleware, renderDynamicComponents, renderSwaggerConfig, setupViews as setupSrcViews } from './modules/app-setup.js';
6
6
  import { setupDatabase } from './modules/database-setup.js';
7
7
  import { setupKafka, setupViews } from './modules/kafka-setup.js';
8
8
  import { setupCaching } from './modules/caching-setup.js';
@@ -33,6 +33,9 @@ export const generateProject = async (config) => {
33
33
  // 6. Render index file (ts/js)
34
34
  await renderIndexFile(templatePath, targetDir, config);
35
35
 
36
+ // 6a. Render error middleware
37
+ await renderErrorMiddleware(templatePath, targetDir, config);
38
+
36
39
  // 7. Render Dynamic Components (Controllers/Repos/Server)
37
40
  await renderDynamicComponents(templatePath, targetDir, config);
38
41
 
@@ -22,6 +22,40 @@ export const renderIndexFile = async (templatePath, targetDir, config) => {
22
22
  }
23
23
  };
24
24
 
25
+ export const renderErrorMiddleware = async (templatePath, targetDir, config) => {
26
+ const { language, architecture } = config;
27
+ const errName = language === 'TypeScript' ? 'error.middleware.ts' : 'error.middleware.js';
28
+ const errTemplateName = `${errName}.ejs`;
29
+
30
+ if (architecture === 'MVC') {
31
+ // MVC: render from target's src/utils/ (the .ejs copy put there by copyBaseStructure)
32
+ const ejsCopy = path.join(targetDir, 'src/utils', errTemplateName);
33
+ const dest = path.join(targetDir, 'src/utils', errName);
34
+ if (await fs.pathExists(ejsCopy)) {
35
+ await fs.writeFile(dest, await fs.readFile(ejsCopy, 'utf-8'));
36
+ await fs.remove(ejsCopy);
37
+ }
38
+ } else {
39
+ // Clean Architecture: render from target's src/utils/ (the .ejs copy put there by copyBaseStructure)
40
+ const utilsEjsCopy = path.join(targetDir, 'src/utils', errTemplateName);
41
+ const utilsDest = path.join(targetDir, 'src/utils', errName);
42
+ await fs.ensureDir(path.join(targetDir, 'src/utils'));
43
+ if (await fs.pathExists(utilsEjsCopy)) {
44
+ await fs.writeFile(utilsDest, await fs.readFile(utilsEjsCopy, 'utf-8'));
45
+ await fs.remove(utilsEjsCopy);
46
+ }
47
+ // Also render the middlewares version if present (NOT removing from template)
48
+ const mwDir = path.join(targetDir, 'src/infrastructure/webserver/middlewares');
49
+ const mwEjsCopy = path.join(mwDir, errTemplateName);
50
+ const mwDest = path.join(mwDir, errName);
51
+ if (await fs.pathExists(mwEjsCopy)) {
52
+ await fs.ensureDir(mwDir);
53
+ await fs.writeFile(mwDest, await fs.readFile(mwEjsCopy, 'utf-8'));
54
+ await fs.remove(mwEjsCopy);
55
+ }
56
+ }
57
+ };
58
+
25
59
  export const renderDynamicComponents = async (templatePath, targetDir, config) => {
26
60
  const { architecture, language, database, caching } = config;
27
61
 
@@ -127,6 +161,10 @@ export const renderSwaggerConfig = async (templatesDir, targetDir, config) => {
127
161
 
128
162
  // Check for Swagger config template (typically in src/config/swagger.ts.ejs)
129
163
  const swaggerTsTemplate = path.join(targetDir, 'src', 'config', 'swagger.ts.ejs');
164
+ // MVC JS uses swagger.js.ejs in src/config/
165
+ const swaggerJsTemplate = path.join(targetDir, 'src', 'config', 'swagger.js.ejs');
166
+ // Clean Arch JS uses swagger.js.ejs in src/infrastructure/webserver/
167
+ const swaggerJsCleanTemplate = path.join(targetDir, 'src', 'infrastructure', 'webserver', 'swagger.js.ejs');
130
168
 
131
169
  // Ensure config directory exists
132
170
  let configDir = path.join(targetDir, 'src', 'config');
@@ -146,13 +184,26 @@ export const renderSwaggerConfig = async (templatesDir, targetDir, config) => {
146
184
  const content = ejs.render(await fs.readFile(swaggerTsTemplate, 'utf-8'), { communication });
147
185
  await fs.writeFile(path.join(targetDir, 'src', 'config', 'swagger.ts'), content);
148
186
  }
187
+
188
+ // MVC JS: render swagger.js.ejs → swagger.js
189
+ if (await fs.pathExists(swaggerJsTemplate)) {
190
+ const content = await fs.readFile(swaggerJsTemplate, 'utf-8');
191
+ await fs.writeFile(path.join(targetDir, 'src', 'config', 'swagger.js'), content);
192
+ }
193
+
194
+ // Clean Arch JS: render swagger.js.ejs → swagger.js in webserver/
195
+ if (await fs.pathExists(swaggerJsCleanTemplate)) {
196
+ const content = await fs.readFile(swaggerJsCleanTemplate, 'utf-8');
197
+ await fs.writeFile(path.join(targetDir, 'src', 'infrastructure', 'webserver', 'swagger.js'), content);
198
+ }
149
199
  }
150
200
 
151
- // Always remove the template after processing or if not needed
152
- if (await fs.pathExists(swaggerTsTemplate)) {
153
- await fs.remove(swaggerTsTemplate);
154
- }
155
- // Also cleanup yml template if not REST APIs since copyBaseSturcture copies it earlier
201
+ // Always remove the .ejs template after processing (or if non-REST, just delete it)
202
+ if (await fs.pathExists(swaggerTsTemplate)) await fs.remove(swaggerTsTemplate);
203
+ if (await fs.pathExists(swaggerJsTemplate)) await fs.remove(swaggerJsTemplate);
204
+ if (await fs.pathExists(swaggerJsCleanTemplate)) await fs.remove(swaggerJsCleanTemplate);
205
+
206
+ // Also cleanup yml template if not REST APIs since copyBaseStructure copies it earlier
156
207
  const swaggerYmlDestPath = path.join(targetDir, 'src', 'config', 'swagger.yml.ejs');
157
208
  if (await fs.pathExists(swaggerYmlDestPath)) {
158
209
  await fs.remove(swaggerYmlDestPath);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodejs-quickstart-structure",
3
- "version": "1.10.1",
3
+ "version": "1.11.1",
4
4
  "type": "module",
5
5
  "description": "A CLI to scaffold Node.js microservices with MVC or Clean Architecture",
6
6
  "main": "bin/index.js",
@@ -0,0 +1,14 @@
1
+ class ApiError extends Error {
2
+ constructor(statusCode, message, isOperational = true, stack = '') {
3
+ super(message);
4
+ this.statusCode = statusCode;
5
+ this.isOperational = isOperational;
6
+ if (stack) {
7
+ this.stack = stack;
8
+ } else {
9
+ Error.captureStackTrace(this, this.constructor);
10
+ }
11
+ }
12
+ }
13
+
14
+ module.exports = { ApiError };
@@ -0,0 +1,10 @@
1
+ const { ApiError } = require('./ApiError');
2
+ const { HTTP_STATUS } = require('../utils/httpCodes');
3
+
4
+ class BadRequestError extends ApiError {
5
+ constructor(message = 'Bad request') {
6
+ super(HTTP_STATUS.BAD_REQUEST, message);
7
+ }
8
+ }
9
+
10
+ module.exports = { BadRequestError };
@@ -0,0 +1,10 @@
1
+ const { ApiError } = require('./ApiError');
2
+ const { HTTP_STATUS } = require('../utils/httpCodes');
3
+
4
+ class NotFoundError extends ApiError {
5
+ constructor(message = 'Resource not found') {
6
+ super(HTTP_STATUS.NOT_FOUND, message);
7
+ }
8
+ }
9
+
10
+ module.exports = { NotFoundError };
@@ -0,0 +1,29 @@
1
+ const logger = require('../../log/logger');
2
+ const { ApiError } = require('../../../errors/ApiError');
3
+ const HTTP_STATUS = require('../../../utils/httpCodes');
4
+
5
+ const errorMiddleware = (err, req, res, _) => {
6
+ let error = err;
7
+
8
+ if (!(error instanceof ApiError)) {
9
+ const statusCode = err.statusCode || HTTP_STATUS.INTERNAL_SERVER_ERROR;
10
+ const message = error.message || 'Internal Server Error';
11
+ error = new ApiError(statusCode, message, false, err.stack);
12
+ }
13
+
14
+ const { statusCode, message } = error;
15
+
16
+ if (statusCode === HTTP_STATUS.INTERNAL_SERVER_ERROR) {
17
+ logger.error(`${statusCode} - ${message} - ${req.originalUrl} - ${req.method} - ${req.ip}`);
18
+ logger.error(error.stack || 'No stack trace');
19
+ }
20
+
21
+ res.status(statusCode).json({
22
+ statusCode,
23
+ message,
24
+ ...(process.env.NODE_ENV === 'development' && { stack: error.stack }),
25
+ });
26
+ };
27
+
28
+ module.exports = { errorMiddleware };
29
+
@@ -3,6 +3,7 @@ const cors = require('cors');
3
3
  require('dotenv').config();
4
4
  const logger = require('../log/logger');
5
5
  const morgan = require('morgan');
6
+ const { errorMiddleware } = require('./middlewares/error.middleware');
6
7
  <%_ if (communication === 'REST APIs') { -%>const apiRoutes = require('../../interfaces/routes/api');<%_ } -%>
7
8
  <%_ if (communication === 'REST APIs') { -%>
8
9
  const swaggerUi = require('swagger-ui-express');
@@ -12,6 +13,8 @@ const swaggerSpecs = require('./swagger');
12
13
  const { ApolloServer } = require('@apollo/server');
13
14
  const { expressMiddleware } = require('@apollo/server/express4');
14
15
  const { ApolloServerPluginLandingPageLocalDefault } = require('@apollo/server/plugin/landingPage/default');
16
+ const { unwrapResolverError } = require('@apollo/server/errors');
17
+ const { ApiError } = require('../../errors/ApiError');
15
18
  const { typeDefs, resolvers } = require('../../interfaces/graphql');
16
19
  const { gqlContext } = require('../../interfaces/graphql/context');
17
20
  <%_ } -%>
@@ -34,6 +37,25 @@ const startServer = async (port) => {
34
37
  typeDefs,
35
38
  resolvers,
36
39
  plugins: [ApolloServerPluginLandingPageLocalDefault({ embed: true })],
40
+ formatError: (formattedError, error) => {
41
+ const originalError = unwrapResolverError(error);
42
+ if (originalError instanceof ApiError) {
43
+ return {
44
+ ...formattedError,
45
+ message: originalError.message,
46
+ extensions: {
47
+ ...formattedError.extensions,
48
+ code: originalError.statusCode.toString(),
49
+ }
50
+ };
51
+ }
52
+
53
+ logger.error(`GraphQL Error: ${formattedError.message}`);
54
+ if (originalError && originalError.stack && process.env.NODE_ENV === 'development') {
55
+ logger.error(originalError.stack);
56
+ }
57
+ return formattedError;
58
+ },
37
59
  });
38
60
  await server.start();
39
61
  app.use('/graphql', expressMiddleware(server, { context: gqlContext }));
@@ -42,6 +64,8 @@ const startServer = async (port) => {
42
64
  res.json({ status: 'UP' });
43
65
  });
44
66
 
67
+ app.use(errorMiddleware);
68
+
45
69
  app.listen(port, () => {
46
70
  logger.info(`Server running on port ${port}`);
47
71
  });
@@ -33,23 +33,20 @@ class UserController {
33
33
  }
34
34
  }
35
35
  <% } else { -%>
36
- getUsers(req, res) {
36
+ getUsers(req, res, next) {
37
37
  this.getAllUsersUseCase.execute()
38
38
  .then(users => res.json(users))
39
- .catch(err => {
40
- logger.error('Error getting users:', err);
41
- res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({ error: err.message });
42
- });
39
+ .catch(next);
43
40
  }
44
41
 
45
- async createUser(req, res) {
42
+ async createUser(req, res, next) {
46
43
  const { name, email } = req.body;
47
44
  try {
48
45
  const user = await this.createUserUseCase.execute(name, email);
49
46
  res.status(HTTP_STATUS.CREATED).json(user);
50
47
  } catch (error) {
51
48
  logger.error('Error creating user:', error);
52
- res.status(HTTP_STATUS.BAD_REQUEST).json({ error: error.message });
49
+ next(error);
53
50
  }
54
51
  }
55
52
  <% } -%>
@@ -1,4 +1,3 @@
1
- const { GraphQLError } = require('graphql');
2
1
  const UserController = require('../../controllers/userController');
3
2
 
4
3
  const userController = new UserController();
@@ -6,20 +5,12 @@ const userController = new UserController();
6
5
  const userResolvers = {
7
6
  Query: {
8
7
  getAllUsers: async () => {
9
- try {
10
- return await userController.getUsers();
11
- } catch (error) {
12
- throw new GraphQLError(error.message || 'Internal server error', { extensions: { code: 'INTERNAL_SERVER_ERROR' } });
13
- }
8
+ return await userController.getUsers();
14
9
  }
15
10
  },
16
11
  Mutation: {
17
12
  createUser: async (_, { name, email }) => {
18
- try {
19
- return await userController.createUser({ name, email });
20
- } catch (error) {
21
- throw new GraphQLError(error.message || 'Internal server error', { extensions: { code: 'INTERNAL_SERVER_ERROR' } });
22
- }
13
+ return await userController.createUser({ name, email });
23
14
  }
24
15
  }
25
16
  };
@@ -4,7 +4,7 @@ const UserController = require('../controllers/userController');
4
4
 
5
5
  const userController = new UserController();
6
6
 
7
- router.post('/users', (req, res) => userController.createUser(req, res));
8
- router.get('/users', (req, res) => userController.getUsers(req, res));
7
+ router.post('/users', (req, res, next) => userController.createUser(req, res, next));
8
+ router.get('/users', (req, res, next) => userController.getUsers(req, res, next));
9
9
 
10
10
  module.exports = router;
@@ -0,0 +1,15 @@
1
+ export class ApiError extends Error {
2
+ statusCode: number;
3
+ isOperational: boolean;
4
+
5
+ constructor(statusCode: number, message: string, isOperational = true, stack = '') {
6
+ super(message);
7
+ this.statusCode = statusCode;
8
+ this.isOperational = isOperational;
9
+ if (stack) {
10
+ this.stack = stack;
11
+ } else {
12
+ Error.captureStackTrace(this, this.constructor);
13
+ }
14
+ }
15
+ }
@@ -0,0 +1,8 @@
1
+ import { ApiError } from './ApiError';
2
+ import { HTTP_STATUS } from '@/utils/httpCodes';
3
+
4
+ export class BadRequestError extends ApiError {
5
+ constructor(message = 'Bad request') {
6
+ super(HTTP_STATUS.BAD_REQUEST, message);
7
+ }
8
+ }
@@ -0,0 +1,8 @@
1
+ import { ApiError } from './ApiError';
2
+ import { HTTP_STATUS } from '@/utils/httpCodes';
3
+
4
+ export class NotFoundError extends ApiError {
5
+ constructor(message = 'Resource not found') {
6
+ super(HTTP_STATUS.NOT_FOUND, message);
7
+ }
8
+ }
@@ -6,6 +6,7 @@ import rateLimit from 'express-rate-limit';
6
6
  import dotenv from 'dotenv';
7
7
  import logger from '@/infrastructure/log/logger';
8
8
  import morgan from 'morgan';
9
+ import { errorMiddleware } from '@/utils/error.middleware';
9
10
  <%_ if (communication === 'REST APIs') { -%>import userRoutes from '@/interfaces/routes/userRoutes';<%_ } -%>
10
11
  <% if (communication === 'REST APIs') { -%>
11
12
  import swaggerUi from 'swagger-ui-express';
@@ -15,6 +16,8 @@ import swaggerSpecs from '@/config/swagger';<% } -%>
15
16
  import { ApolloServer } from '@apollo/server';
16
17
  import { expressMiddleware } from '@apollo/server/express4';
17
18
  import { ApolloServerPluginLandingPageLocalDefault } from '@apollo/server/plugin/landingPage/default';
19
+ import { unwrapResolverError } from '@apollo/server/errors';
20
+ import { ApiError } from '@/errors/ApiError';
18
21
  import { typeDefs, resolvers } from '@/interfaces/graphql';
19
22
  import { gqlContext, MyContext } from '@/interfaces/graphql/context';
20
23
  <% } -%>
@@ -66,10 +69,30 @@ const startServer = async () => {
66
69
  typeDefs,
67
70
  resolvers,
68
71
  plugins: [ApolloServerPluginLandingPageLocalDefault({ embed: true })],
72
+ formatError: (formattedError, error) => {
73
+ const originalError = unwrapResolverError(error);
74
+ if (originalError instanceof ApiError) {
75
+ return {
76
+ ...formattedError,
77
+ message: originalError.message,
78
+ extensions: {
79
+ ...formattedError.extensions,
80
+ code: originalError.statusCode.toString(),
81
+ }
82
+ };
83
+ }
84
+
85
+ logger.error(`GraphQL Error: ${formattedError.message}`);
86
+ if (originalError instanceof Error && originalError.stack && process.env.NODE_ENV === 'development') {
87
+ logger.error(originalError.stack);
88
+ }
89
+ return formattedError;
90
+ },
69
91
  });
70
92
  await server.start();
71
93
  app.use('/graphql', expressMiddleware(server, { context: gqlContext }));
72
94
  <%_ } -%>
95
+ app.use(errorMiddleware);
73
96
  app.listen(port, () => {
74
97
  logger.info(`Server running on port ${port}`);
75
98
  <%_ if (communication === 'Kafka') { -%>
@@ -1,5 +1,5 @@
1
1
  <% if (communication !== 'GraphQL') { -%>
2
- import { Request, Response } from 'express';
2
+ import { Request, Response, NextFunction } from 'express';
3
3
  import { HTTP_STATUS } from '@/utils/httpCodes';
4
4
  <% } -%>
5
5
  import { UserRepository } from '@/infrastructure/repositories/UserRepository';
@@ -25,7 +25,7 @@ export class UserController {
25
25
  return user;
26
26
  } catch (error: unknown) {
27
27
  const message = error instanceof Error ? error.message : 'Unknown error';
28
- logger.error('UserController Error:', message);
28
+ logger.error('UserController CreateUser Error:', message);
29
29
  throw error;
30
30
  }
31
31
  }
@@ -36,37 +36,31 @@ export class UserController {
36
36
  return users;
37
37
  } catch (error: unknown) {
38
38
  const message = error instanceof Error ? error.message : 'Unknown error';
39
- logger.error('UserController Error:', message);
39
+ logger.error('UserController GetUsers Error:', message);
40
40
  throw error;
41
41
  }
42
42
  }
43
43
  <% } else { -%>
44
- async createUser(req: Request, res: Response) {
44
+ async createUser(req: Request, res: Response, next: NextFunction) {
45
45
  try {
46
46
  const { name, email } = req.body;
47
47
  const user = await this.createUserUseCase.execute(name, email);
48
48
  res.status(HTTP_STATUS.CREATED).json(user);
49
- } catch (error) {
50
- logger.error('UserController Error:', error);
51
- if (error instanceof Error) {
52
- res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({ error: error.message });
53
- } else {
54
- res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({ error: 'Unknown error occurred' });
55
- }
49
+ } catch (error: unknown) {
50
+ const message = error instanceof Error ? error.message : 'Unknown error';
51
+ logger.error('UserController CreateUser Error:', message);
52
+ next(error);
56
53
  }
57
54
  }
58
55
 
59
- async getUsers(req: Request, res: Response) {
56
+ async getUsers(req: Request, res: Response, next: NextFunction) {
60
57
  try {
61
58
  const users = await this.getAllUsersUseCase.execute();
62
59
  res.json(users);
63
- } catch (error) {
64
- logger.error('UserController Error:', error);
65
- if (error instanceof Error) {
66
- res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({ error: error.message });
67
- } else {
68
- res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({ error: 'Unknown error occurred' });
69
- }
60
+ } catch (error: unknown) {
61
+ const message = error instanceof Error ? error.message : 'Unknown error';
62
+ logger.error('UserController GetUsers Error:', message);
63
+ next(error);
70
64
  }
71
65
  }
72
66
  <% } -%>
@@ -1,4 +1,3 @@
1
- import { GraphQLError } from 'graphql';
2
1
  import { UserController } from '@/interfaces/controllers/userController';
3
2
 
4
3
  const userController = new UserController();
@@ -6,22 +5,14 @@ const userController = new UserController();
6
5
  export const userResolvers = {
7
6
  Query: {
8
7
  getAllUsers: async () => {
9
- try {
10
- return await userController.getUsers();
11
- } catch (error: unknown) {
12
- const message = error instanceof Error ? error.message : 'Internal server error';
13
- throw new GraphQLError(message, { extensions: { code: 'INTERNAL_SERVER_ERROR' } });
14
- }
8
+ const users = await userController.getUsers();
9
+ return users;
15
10
  }
16
11
  },
17
12
  Mutation: {
18
13
  createUser: async (_: unknown, { name, email }: { name: string, email: string }) => {
19
- try {
20
- return await userController.createUser({ name, email });
21
- } catch (error: unknown) {
22
- const message = error instanceof Error ? error.message : 'Internal server error';
23
- throw new GraphQLError(message, { extensions: { code: 'INTERNAL_SERVER_ERROR' } });
24
- }
14
+ const user = await userController.createUser({ name, email });
15
+ return user;
25
16
  }
26
17
  }
27
18
  };