nodejs-quickstart-structure 1.10.1 → 1.11.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 (40) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +4 -0
  3. package/docs/generatorFlow.md +65 -23
  4. package/lib/generator.js +4 -1
  5. package/lib/modules/app-setup.js +56 -5
  6. package/package.json +1 -1
  7. package/templates/clean-architecture/js/src/errors/ApiError.js +14 -0
  8. package/templates/clean-architecture/js/src/errors/BadRequestError.js +10 -0
  9. package/templates/clean-architecture/js/src/errors/NotFoundError.js +10 -0
  10. package/templates/clean-architecture/js/src/infrastructure/webserver/middlewares/error.middleware.js +29 -0
  11. package/templates/clean-architecture/js/src/infrastructure/webserver/server.js.ejs +24 -0
  12. package/templates/clean-architecture/js/src/interfaces/controllers/userController.js.ejs +4 -7
  13. package/templates/clean-architecture/js/src/interfaces/graphql/resolvers/user.resolvers.js.ejs +2 -11
  14. package/templates/clean-architecture/js/src/interfaces/routes/api.js +2 -2
  15. package/templates/clean-architecture/ts/src/errors/ApiError.ts +15 -0
  16. package/templates/clean-architecture/ts/src/errors/BadRequestError.ts +8 -0
  17. package/templates/clean-architecture/ts/src/errors/NotFoundError.ts +8 -0
  18. package/templates/clean-architecture/ts/src/index.ts.ejs +23 -0
  19. package/templates/clean-architecture/ts/src/interfaces/controllers/userController.ts.ejs +13 -19
  20. package/templates/clean-architecture/ts/src/interfaces/graphql/resolvers/user.resolvers.ts.ejs +4 -13
  21. package/templates/clean-architecture/ts/src/interfaces/routes/userRoutes.ts +4 -3
  22. package/templates/clean-architecture/ts/src/utils/error.middleware.ts.ejs +27 -0
  23. package/templates/common/package.json.ejs +5 -6
  24. package/templates/mvc/js/src/controllers/userController.js.ejs +5 -4
  25. package/templates/mvc/js/src/errors/ApiError.js +14 -0
  26. package/templates/mvc/js/src/errors/BadRequestError.js +10 -0
  27. package/templates/mvc/js/src/errors/NotFoundError.js +10 -0
  28. package/templates/mvc/js/src/graphql/resolvers/user.resolvers.js.ejs +2 -11
  29. package/templates/mvc/js/src/index.js.ejs +23 -0
  30. package/templates/mvc/js/src/utils/error.middleware.js +28 -0
  31. package/templates/mvc/ts/src/controllers/userController.ts.ejs +6 -14
  32. package/templates/mvc/ts/src/errors/ApiError.ts +15 -0
  33. package/templates/mvc/ts/src/errors/BadRequestError.ts +8 -0
  34. package/templates/mvc/ts/src/errors/NotFoundError.ts +8 -0
  35. package/templates/mvc/ts/src/graphql/resolvers/user.resolvers.ts.ejs +4 -13
  36. package/templates/mvc/ts/src/index.ts.ejs +23 -0
  37. package/templates/mvc/ts/src/routes/api.ts +3 -3
  38. package/templates/mvc/ts/src/utils/error.middleware.ts.ejs +27 -0
  39. /package/templates/clean-architecture/js/src/infrastructure/webserver/{swagger.js → swagger.js.ejs} +0 -0
  40. /package/templates/mvc/js/src/config/{swagger.js → swagger.js.ejs} +0 -0
package/CHANGELOG.md CHANGED
@@ -5,6 +5,24 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.11.0] - 2026-03-02
9
+ ### Added
10
+ - **Centralized Error Handling Mechanism:** All generated projects now include a standardized, predictable error response structure for both REST APIs and GraphQL communication types.
11
+ - New `src/errors/` directory with custom error classes: `ApiError`, `NotFoundError`, `BadRequestError`.
12
+ - New `error.middleware.{ts|js}` global error handler placed at the end of the Express middleware chain in `src/utils/` (MVC) and `src/utils/` + `src/infrastructure/webserver/middlewares/` (Clean Architecture).
13
+ - Integrates `winston` logger to automatically log 500-level errors to persistent log files.
14
+ - All controllers updated to pass errors via `next(error)` instead of manually sending responses.
15
+ - **GraphQL:** Apollo Server `formatError` hook configured with `unwrapResolverError` to intercept resolver errors and map `ApiError` instances to structured GraphQL extension codes.
16
+ - **REST APIs:** Express error middleware returns a consistent `{ statusCode, message, stack? }` JSON body.
17
+ - **Error Response Standardization:** All error responses follow the same schema regardless of database, caching, or communication type selected.
18
+
19
+ ### Fixed
20
+ - Fixed `swagger.js` / `swagger.yml` being incorrectly generated for non-REST API configurations (GraphQL, Kafka). Converted static `swagger.js` to `swagger.js.ejs` in MVC JS and Clean Architecture JS templates so `renderSwaggerConfig` can conditionally control its generation.
21
+ - Fixed `userRoutes.ts` in Clean Architecture TypeScript template not passing `NextFunction` to controller methods, causing `TypeScript TS2554: Expected 3 arguments` compile errors during Docker builds.
22
+ - Fixed incorrect relative import paths (`../../../../`) in Clean Architecture JS `error.middleware.js` — changed to correct 3-level relative paths (`../../../`).
23
+ - Fixed `HTTP_STATUS` being imported without destructuring from `httpCodes.js` (which uses a plain `module.exports = HTTP_STATUS` default export), causing `Cannot read properties of undefined` runtime errors.
24
+ - Fixed `renderErrorMiddleware` in `app-setup.js` deleting from the **template source directory** instead of the generated project's target directory. This caused error middleware templates to disappear from disk after the first test run, breaking all subsequent generations.
25
+
8
26
  ## [1.10.1] - 2026-03-02
9
27
  ### Added
10
28
  - Roadmap & Upcoming Features. **[View our Public Roadmap on Trello](https://trello.com/b/TPTo8ylF/nodejs-quickstart-structure-product)**
package/README.md CHANGED
@@ -19,6 +19,7 @@ A powerful CLI tool to scaffold production-ready Node.js microservices with buil
19
19
  - **Database Integration**: Pre-configured setup for **MySQL**, **PostgreSQL**, or **MongoDB**.
20
20
  - **Communication Flow**: Scaffold APIs using **REST**, **GraphQL** (with Apollo Server), or **Kafka** (event-driven).
21
21
  - **Caching Layer**: Choose between **Redis** or built-in **Memory Cache** for data caching.
22
+ - **Centralized Error Handling**: Every project ships with a global error handler, custom error classes (`ApiError`, `NotFoundError`, `BadRequestError`), and structured JSON error responses — consistent across REST & GraphQL.
22
23
  - **Dockerized**: Automatically generates `docker-compose.yml` for DB, Kafka, Redis, and Zookeeper.
23
24
  - **Database Migrations/Schemas**: Integrated **Flyway** for SQL migrations or **Mongoose** schemas for MongoDB.
24
25
  - **Professional Standards**: Generated projects come with highly professional, industry-standard tooling.
@@ -29,6 +30,7 @@ We don't just generate boilerplate; we generate **production-ready** foundations
29
30
 
30
31
  - **🔍 Code Quality**: Pre-configured `Eslint` and `Prettier` for consistent coding standards.
31
32
  - **🛡️ Security**: Built-in `Helmet`, `HPP`, `CORS`, and Rate-Limiting middleware.
33
+ - **🚨 Error Handling**: Centralized global error middleware with custom error classes and structured `{ statusCode, message }` JSON responses. GraphQL uses Apollo's `formatError` hook; REST uses Express error middleware. Both integrate with Winston for automatic 500-level logging.
32
34
  - **🧪 Testing Strategy**: Integrated `Jest` and `Supertest` setup for Unit and Integration testing.
33
35
  - **🔄 CI/CD Integration**: Pre-configured workflows for **GitHub Actions**, **Jenkins**, and **GitLab CI**.
34
36
  - **⚓ Git Hooks**: `Husky` and `Lint-Staged` to ensure no bad code is ever committed.
@@ -89,6 +91,8 @@ The CLI will guide you through the following steps:
89
91
  The generated project will include:
90
92
 
91
93
  - `src/`: Source code (controllers, routes, services/use-cases).
94
+ - `src/errors/`: Custom error classes — `ApiError`, `NotFoundError`, `BadRequestError`.
95
+ - `src/utils/error.middleware.{ts|js}`: Global Express error handler (logs 500s, returns `{ statusCode, message }`).
92
96
  - `flyway/sql/`: SQL migration scripts (if SQL database selected).
93
97
  - `docker-compose.yml`: Services configuration for DB, Flyway, and Kafka.
94
98
  - `package.json`: Dependencies and scripts (`start`, `dev`, `build`).
@@ -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.0",
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
  };
@@ -1,10 +1,11 @@
1
- import { Router, Request, Response } from 'express';
1
+ import { Router, Request, Response, NextFunction } from 'express';
2
2
  import { UserController } from '@/interfaces/controllers/userController';
3
3
 
4
4
  const router = Router();
5
5
  const userController = new UserController();
6
6
 
7
- router.post('/', (req: Request, res: Response) => userController.createUser(req, res));
8
- router.get('/', (req: Request, res: Response) => userController.getUsers(req, res));
7
+ router.post('/', (req: Request, res: Response, next: NextFunction) => userController.createUser(req, res, next));
8
+ router.get('/', (req: Request, res: Response, next: NextFunction) => userController.getUsers(req, res, next));
9
9
 
10
10
  export default router;
11
+
@@ -0,0 +1,27 @@
1
+ import { Request, Response } from 'express';
2
+ import logger from '@/infrastructure/log/logger';
3
+ import { ApiError } from '@/errors/ApiError';
4
+ import { HTTP_STATUS } from '@/utils/httpCodes';
5
+
6
+ export const errorMiddleware = (err: Error, req: Request, res: Response, _: unknown) => {
7
+ let error = err;
8
+
9
+ if (!(error instanceof ApiError)) {
10
+ const statusCode = HTTP_STATUS.INTERNAL_SERVER_ERROR;
11
+ const message = error.message || 'Internal Server Error';
12
+ error = new ApiError(statusCode, message, false, err.stack);
13
+ }
14
+
15
+ const { statusCode, message } = error as ApiError;
16
+
17
+ if (statusCode === HTTP_STATUS.INTERNAL_SERVER_ERROR) {
18
+ logger.error(`${statusCode} - ${message} - ${req.originalUrl} - ${req.method} - ${req.ip}`);
19
+ logger.error(error.stack || 'No stack trace');
20
+ }
21
+
22
+ res.status(statusCode).json({
23
+ statusCode,
24
+ message,
25
+ ...(process.env.NODE_ENV === 'development' && { stack: error.stack }),
26
+ });
27
+ };
@@ -11,12 +11,11 @@
11
11
  "lint:fix": "eslint . --fix",
12
12
  "format": "prettier --write .",
13
13
  "prepare": "node -e \"try { require('child_process').execSync('husky install'); } catch (e) { console.log('Not a git repository, skipping husky install'); }\"",
14
+ <% if (database === 'MongoDB') { %> "migrate": "migrate-mongo up",
15
+ <% } -%>
14
16
  "test": "jest",
15
17
  "test:watch": "jest --watch",
16
18
  "test:coverage": "jest --coverage"
17
- <%_ if (database === 'MongoDB') { -%>,
18
- "migrate": "migrate-mongo up"
19
- <%_ } -%>
20
19
  },
21
20
  "dependencies": {
22
21
  "express": "^4.18.2",
@@ -80,9 +79,9 @@
80
79
  "eslint-config-prettier": "^10.0.1",
81
80
  "husky": "^8.0.3",
82
81
  "lint-staged": "^15.4.3"<% if (language === 'TypeScript') { %>,
83
- "typescript-eslint": "^8.24.1",
84
- <% if (communication === 'REST APIs') { %> "@types/swagger-ui-express": "^4.1.6",
85
- "@types/yamljs": "^0.2.34",<% } %>
82
+ "typescript-eslint": "^8.24.1",<%_ if (communication === 'REST APIs') { %>
83
+ "@types/swagger-ui-express": "^4.1.6",
84
+ "@types/yamljs": "^0.2.34",<%_ } -%>
86
85
  "jest": "^29.7.0",
87
86
  "ts-jest": "^29.2.5",
88
87
  "@types/jest": "^29.5.14",
@@ -61,7 +61,7 @@ const createUser = async (data) => {
61
61
  }
62
62
  };
63
63
  <% } else { -%>
64
- const getUsers = async (req, res) => {
64
+ const getUsers = async (req, res, next) => {
65
65
  try {
66
66
  <%_ if (caching === 'Redis' || caching === 'Memory Cache') { -%>
67
67
  const users = await cacheService.getOrSet('users:all', async () => {
@@ -85,11 +85,11 @@ const getUsers = async (req, res) => {
85
85
  res.json(users);
86
86
  } catch (error) {
87
87
  logger.error('Error fetching users:', error);
88
- res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({ error: 'Internal Server Error' });
88
+ next(error);
89
89
  }
90
90
  };
91
91
 
92
- const createUser = async (req, res) => {
92
+ const createUser = async (req, res, next) => {
93
93
  try {
94
94
  const { name, email } = req.body;
95
95
  <%_ if (database === 'None') { -%>
@@ -107,7 +107,8 @@ const createUser = async (req, res) => {
107
107
  res.status(HTTP_STATUS.CREATED).json(user);
108
108
  <%_ } -%>
109
109
  } catch (error) {
110
- res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({ error: error.message });
110
+ logger.error('Error creating user:', error);
111
+ next(error);
111
112
  }
112
113
  };
113
114
  <% } -%>
@@ -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 };
@@ -1,23 +1,14 @@
1
- const { GraphQLError } = require('graphql');
2
1
  const userController = require('../../controllers/userController');
3
2
 
4
3
  const userResolvers = {
5
4
  Query: {
6
5
  getAllUsers: async () => {
7
- try {
8
- return await userController.getUsers();
9
- } catch (error) {
10
- throw new GraphQLError(error.message || 'Internal server error', { extensions: { code: 'INTERNAL_SERVER_ERROR' } });
11
- }
6
+ return await userController.getUsers();
12
7
  }
13
8
  },
14
9
  Mutation: {
15
10
  createUser: async (_, { name, email }) => {
16
- try {
17
- return await userController.createUser({ name, email });
18
- } catch (error) {
19
- throw new GraphQLError(error.message || 'Internal server error', { extensions: { code: 'INTERNAL_SERVER_ERROR' } });
20
- }
11
+ return await userController.createUser({ name, email });
21
12
  }
22
13
  }
23
14
  };
@@ -7,6 +7,8 @@ require('dotenv').config();
7
7
  const { ApolloServer } = require('@apollo/server');
8
8
  const { expressMiddleware } = require('@apollo/server/express4');
9
9
  const { ApolloServerPluginLandingPageLocalDefault } = require('@apollo/server/plugin/landingPage/default');
10
+ const { unwrapResolverError } = require('@apollo/server/errors');
11
+ const { ApiError } = require('./errors/ApiError');
10
12
  const { typeDefs, resolvers } = require('./graphql');
11
13
  const { gqlContext } = require('./graphql/context');
12
14
  <% } -%>
@@ -19,6 +21,7 @@ const app = express();
19
21
  const PORT = process.env.PORT || 3000;
20
22
  const logger = require('./utils/logger');
21
23
  const morgan = require('morgan');
24
+ const { errorMiddleware } = require('./utils/error.middleware');
22
25
 
23
26
  app.use(cors());
24
27
  app.use(express.json());
@@ -57,10 +60,30 @@ const startServer = async () => {
57
60
  typeDefs,
58
61
  resolvers,
59
62
  plugins: [ApolloServerPluginLandingPageLocalDefault({ embed: true })],
63
+ formatError: (formattedError, error) => {
64
+ const originalError = unwrapResolverError(error);
65
+ if (originalError instanceof ApiError) {
66
+ return {
67
+ ...formattedError,
68
+ message: originalError.message,
69
+ extensions: {
70
+ ...formattedError.extensions,
71
+ code: originalError.statusCode.toString(),
72
+ }
73
+ };
74
+ }
75
+
76
+ logger.error(`GraphQL Error: ${formattedError.message}`);
77
+ if (originalError && originalError.stack && process.env.NODE_ENV === 'development') {
78
+ logger.error(originalError.stack);
79
+ }
80
+ return formattedError;
81
+ },
60
82
  });
61
83
  await server.start();
62
84
  app.use('/graphql', expressMiddleware(server, { context: gqlContext }));
63
85
  <%_ } -%>
86
+ app.use(errorMiddleware);
64
87
  app.listen(PORT, () => {
65
88
  logger.info(`Server running on port ${PORT}`);
66
89
  <%_ if (communication === 'Kafka') { -%>
@@ -0,0 +1,28 @@
1
+ const logger = require('./logger');
2
+ const { ApiError } = require('../errors/ApiError');
3
+ const HTTP_STATUS = require('./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 };
@@ -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 User from '@/models/User';
@@ -63,7 +63,7 @@ export class UserController {
63
63
  }
64
64
  }
65
65
  <% } else { -%>
66
- async getUsers(req: Request, res: Response) {
66
+ async getUsers(req: Request, res: Response, next: NextFunction) {
67
67
  try {
68
68
  <%_ if (caching === 'Redis' || caching === 'Memory Cache') { -%>
69
69
  const users = await cacheService.getOrSet('users:all', async () => {
@@ -86,16 +86,12 @@ export class UserController {
86
86
  <%_ } -%>
87
87
  res.json(users);
88
88
  } catch (error) {
89
- logger.error('Error fetching users:', error);
90
- if (error instanceof Error) {
91
- res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({ error: error.message });
92
- } else {
93
- res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({ error: 'Unknown error occurred' });
94
- }
89
+ logger.error('Error fetching user:', error);
90
+ next(error);
95
91
  }
96
92
  }
97
93
 
98
- async createUser(req: Request, res: Response) {
94
+ async createUser(req: Request, res: Response, next: NextFunction) {
99
95
  try {
100
96
  const { name, email } = req.body;
101
97
  <%_ if (database === 'None') { -%>
@@ -114,11 +110,7 @@ export class UserController {
114
110
  <%_ } -%>
115
111
  } catch (error) {
116
112
  logger.error('Error creating user:', error);
117
- if (error instanceof Error) {
118
- res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({ error: error.message });
119
- } else {
120
- res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({ error: 'Unknown error occurred' });
121
- }
113
+ next(error);
122
114
  }
123
115
  }
124
116
  <% } -%>
@@ -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
+ }
@@ -1,4 +1,3 @@
1
- import { GraphQLError } from 'graphql';
2
1
  import { UserController } from '@/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
  };
@@ -6,6 +6,7 @@ import rateLimit from 'express-rate-limit';
6
6
  import dotenv from 'dotenv';
7
7
  import logger from '@/utils/logger';
8
8
  import morgan from 'morgan';
9
+ import { errorMiddleware } from '@/utils/error.middleware';
9
10
  <%_ if (communication === 'REST APIs') { -%>
10
11
  import apiRoutes from '@/routes/api';<%_ } -%>
11
12
  <% if (communication === 'REST APIs') { %>
@@ -16,6 +17,8 @@ import swaggerSpecs from '@/config/swagger';<% } -%>
16
17
  import { ApolloServer } from '@apollo/server';
17
18
  import { expressMiddleware } from '@apollo/server/express4';
18
19
  import { ApolloServerPluginLandingPageLocalDefault } from '@apollo/server/plugin/landingPage/default';
20
+ import { unwrapResolverError } from '@apollo/server/errors';
21
+ import { ApiError } from '@/errors/ApiError';
19
22
  import { typeDefs, resolvers } from '@/graphql';
20
23
  import { gqlContext, MyContext } from '@/graphql/context';
21
24
  <% } -%>
@@ -81,10 +84,30 @@ const startServer = async () => {
81
84
  typeDefs,
82
85
  resolvers,
83
86
  plugins: [ApolloServerPluginLandingPageLocalDefault({ embed: true })],
87
+ formatError: (formattedError, error) => {
88
+ const originalError = unwrapResolverError(error);
89
+ if (originalError instanceof ApiError) {
90
+ return {
91
+ ...formattedError,
92
+ message: originalError.message,
93
+ extensions: {
94
+ ...formattedError.extensions,
95
+ code: originalError.statusCode.toString(),
96
+ }
97
+ };
98
+ }
99
+
100
+ logger.error(`GraphQL Error: ${formattedError.message}`);
101
+ if (originalError instanceof Error && originalError.stack && process.env.NODE_ENV === 'development') {
102
+ logger.error(originalError.stack);
103
+ }
104
+ return formattedError;
105
+ },
84
106
  });
85
107
  await server.start();
86
108
  app.use('/graphql', expressMiddleware(server, { context: gqlContext }));
87
109
  <%_ } -%>
110
+ app.use(errorMiddleware);
88
111
  app.listen(port, () => {
89
112
  logger.info(`Server running on port ${port}`);
90
113
  <%_ if (communication === 'Kafka') { -%>
@@ -1,10 +1,10 @@
1
- import { Router, Request, Response } from 'express';
1
+ import { Router, Request, Response, NextFunction } from 'express';
2
2
  import { UserController } from '@/controllers/userController';
3
3
 
4
4
  const router = Router();
5
5
  const userController = new UserController();
6
6
 
7
- router.get('/users', (req: Request, res: Response) => userController.getUsers(req, res));
8
- router.post('/users', (req: Request, res: Response) => userController.createUser(req, res));
7
+ router.get('/users', (req: Request, res: Response, next: NextFunction) => userController.getUsers(req, res, next));
8
+ router.post('/users', (req: Request, res: Response, next: NextFunction) => userController.createUser(req, res, next));
9
9
 
10
10
  export default router;
@@ -0,0 +1,27 @@
1
+ import { Request, Response } from 'express';
2
+ import logger from '@/utils/logger';
3
+ import { ApiError } from '@/errors/ApiError';
4
+ import { HTTP_STATUS } from '@/utils/httpCodes';
5
+
6
+ export const errorMiddleware = (err: Error, req: Request, res: Response, _: unknown) => {
7
+ let error = err;
8
+
9
+ if (!(error instanceof ApiError)) {
10
+ const statusCode = HTTP_STATUS.INTERNAL_SERVER_ERROR;
11
+ const message = error.message || 'Internal Server Error';
12
+ error = new ApiError(statusCode, message, false, err.stack);
13
+ }
14
+
15
+ const { statusCode, message } = error as ApiError;
16
+
17
+ if (statusCode === HTTP_STATUS.INTERNAL_SERVER_ERROR) {
18
+ logger.error(`${statusCode} - ${message} - ${req.originalUrl} - ${req.method} - ${req.ip}`);
19
+ logger.error(error.stack || 'No stack trace');
20
+ }
21
+
22
+ res.status(statusCode).json({
23
+ statusCode,
24
+ message,
25
+ ...(process.env.NODE_ENV === 'development' && { stack: error.stack }),
26
+ });
27
+ };