create-node-prodkit 1.0.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.
- package/README.md +1540 -0
- package/bin/create.js +108 -0
- package/package.json +48 -0
- package/templates/.env.example +28 -0
- package/templates/gitignore +6 -0
- package/templates/package.json +33 -0
- package/templates/src/app.js +21 -0
- package/templates/src/config/app.config.js +25 -0
- package/templates/src/controllers/sample.controller.js +11 -0
- package/templates/src/db/mongo.db.js +14 -0
- package/templates/src/db/mysql.db.js +17 -0
- package/templates/src/db/postgres.db.js +20 -0
- package/templates/src/middlewares/encrypt.middleware.js +4 -0
- package/templates/src/middlewares/error.middleware.js +23 -0
- package/templates/src/middlewares/ip.middleware.js +20 -0
- package/templates/src/middlewares/rateLimit.middleware.js +18 -0
- package/templates/src/models/sample.model.js +0 -0
- package/templates/src/repositories/sample.repository.js +10 -0
- package/templates/src/routes/index.route.js +8 -0
- package/templates/src/routes/sample1.route.js +8 -0
- package/templates/src/server.js +6 -0
- package/templates/src/services/sample.service.js +17 -0
- package/templates/src/utils/asyncHandler.js +5 -0
- package/templates/src/utils/axios.util.js +19 -0
- package/templates/src/utils/log.util.js +51 -0
- package/templates/src/utils/logSanitizer.util.js +50 -0
- package/templates/src/utils/response.util.js +43 -0
- package/templates/src/utils/sensitiveKeys.util.js +3 -0
- package/templates/src/validations/sample.validation.js +0 -0
package/README.md
ADDED
|
@@ -0,0 +1,1540 @@
|
|
|
1
|
+
# Node.js Production-Grade Skeleton
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/create-node-prodkit)
|
|
4
|
+
[](https://nodejs.org)
|
|
5
|
+
|
|
6
|
+
A clean, opinionated Node.js REST API skeleton using **ES Modules**, **Express 5**, and a layered architecture designed for production. Available as an **NPM CLI** — scaffold a fully wired project in one command.
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npx create-node-prodkit project-name
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Table of Contents
|
|
15
|
+
|
|
16
|
+
1. [Prerequisites](#1-prerequisites)
|
|
17
|
+
2. [Getting Started](#2-getting-started)
|
|
18
|
+
3. [Project Structure](#3-project-structure)
|
|
19
|
+
4. [Request Lifecycle (The Full Flow)](#4-request-lifecycle-the-full-flow)
|
|
20
|
+
- [Architecture Diagram](#the-layered-architecture)
|
|
21
|
+
- [Error Path](#the-error-path)
|
|
22
|
+
- [Concrete Traced Example](#concrete-traced-example--get-apiv1users42)
|
|
23
|
+
5. [Configuration](#5-configuration)
|
|
24
|
+
6. [How to Write a Feature — Step by Step](#6-how-to-write-a-feature--step-by-step)
|
|
25
|
+
- [Route](#step-1-create-the-route)
|
|
26
|
+
- [Controller](#step-2-create-the-controller)
|
|
27
|
+
- [Validation Middleware](#step-3-create-validation-middleware)
|
|
28
|
+
- [Service](#step-4-create-the-service)
|
|
29
|
+
- [Repository](#step-5-create-the-repository)
|
|
30
|
+
- [Model](#step-6-create-the-model-optional)
|
|
31
|
+
- [Register the Route](#step-7-register-the-route)
|
|
32
|
+
7. [Utilities — How to Use Each One](#7-utilities--how-to-use-each-one)
|
|
33
|
+
- [asyncHandler](#asynchandler)
|
|
34
|
+
- [ResponseUtil](#responseutil)
|
|
35
|
+
- [logService](#logservice)
|
|
36
|
+
- [apiCall (axios.util)](#apicall-axiosutil)
|
|
37
|
+
- [logSanitizer / sensitiveKeys](#logsanitizer--sensitivekeys)
|
|
38
|
+
8. [Middlewares](#8-middlewares)
|
|
39
|
+
- [rateLimitMiddleware](#ratelimitmiddleware)
|
|
40
|
+
- [ipMiddleware](#ipmiddleware)
|
|
41
|
+
- [errorMiddleware](#errormiddleware)
|
|
42
|
+
- [encryptMiddleware](#encryptmiddleware)
|
|
43
|
+
9. [Database Connections](#9-database-connections)
|
|
44
|
+
10. [Logging System Deep Dive](#10-logging-system-deep-dive)
|
|
45
|
+
11. [Environment Variables Reference](#11-environment-variables-reference)
|
|
46
|
+
12. [Code Standards & Conventions](#12-code-standards--conventions)
|
|
47
|
+
- [Rule 1 — Single Responsibility](#rule-1--single-responsibility-per-layer)
|
|
48
|
+
- [Rule 2 — File & Folder Naming](#rule-2--file--folder-naming)
|
|
49
|
+
- [Rule 3 — Naming Conventions](#rule-3--naming-conventions-in-code)
|
|
50
|
+
- [Rule 4 — Import Order](#rule-4--import-order)
|
|
51
|
+
- [Rule 5 — Exports](#rule-5--exports)
|
|
52
|
+
- [Rule 6 — Async / Await](#rule-6--async--await)
|
|
53
|
+
- [Rule 7 — Error Handling](#rule-7--error-handling)
|
|
54
|
+
- [Rule 8 — Responses](#rule-8--responses)
|
|
55
|
+
- [Rule 9 — Logging](#rule-9--logging)
|
|
56
|
+
- [Rule 10 — Configuration](#rule-10--configuration)
|
|
57
|
+
- [Rule 11 — Small & Flat Functions](#rule-11--keep-functions-small-and-flat)
|
|
58
|
+
- [Rule 12 — Comments](#rule-12--comments)
|
|
59
|
+
- [Rule 13 — Repository Shape](#rule-13--repository-shape)
|
|
60
|
+
- [Rule 14 — Never Pass req/res to Services](#rule-14--never-pass-req-or-res-to-a-service)
|
|
61
|
+
13. [What NOT to Do](#13-what-not-to-do)
|
|
62
|
+
14. [CLI Package — How It Works](#14-cli-package--how-it-works)
|
|
63
|
+
15. [Maintaining & Publishing Updates](#15-maintaining--publishing-updates)
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## 1. Prerequisites
|
|
68
|
+
|
|
69
|
+
| Tool | Minimum Version |
|
|
70
|
+
|------|----------------|
|
|
71
|
+
| Node.js | 18+ |
|
|
72
|
+
| npm | 9+ |
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## 2. Getting Started
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
# 1. Clone or copy the skeleton
|
|
80
|
+
cd project-name
|
|
81
|
+
|
|
82
|
+
# 2. Install dependencies
|
|
83
|
+
npm install
|
|
84
|
+
|
|
85
|
+
# 3. Create your environment file
|
|
86
|
+
cp .env.example .env # or create .env manually (see section 11)
|
|
87
|
+
|
|
88
|
+
# 4. Start the dev server (with hot reload)
|
|
89
|
+
npm run dev
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
The server starts at `http://localhost:3000` by default.
|
|
93
|
+
|
|
94
|
+
> **How hot reload works:** `nodemon` watches the `src/` directory. Any `.js` or `.json` file change triggers an automatic restart. This is configured in `package.json` under `nodemonConfig`.
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## 3. Project Structure
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
src/
|
|
102
|
+
├── server.js → Entry point: starts the HTTP server
|
|
103
|
+
├── app.js → Express app setup, middleware stack, routes
|
|
104
|
+
│
|
|
105
|
+
├── config/
|
|
106
|
+
│ └── app.config.js → All config values read from .env in one place
|
|
107
|
+
│
|
|
108
|
+
├── routes/
|
|
109
|
+
│ ├── index.route.js → Root router — mounts all feature routers
|
|
110
|
+
│ └── sample1.route.js → Feature-specific routes
|
|
111
|
+
│
|
|
112
|
+
├── controllers/
|
|
113
|
+
│ └── sample.controller.js → Handles req/res, calls services
|
|
114
|
+
│
|
|
115
|
+
├── services/
|
|
116
|
+
│ └── sample.service.js → Business logic layer
|
|
117
|
+
│
|
|
118
|
+
├── repositories/
|
|
119
|
+
│ └── sample.repository.js → Data access layer (DB or external API)
|
|
120
|
+
│
|
|
121
|
+
├── models/
|
|
122
|
+
│ └── sample.model.js → DB model definitions (Mongoose, Sequelize, etc.)
|
|
123
|
+
│
|
|
124
|
+
├── middlewares/
|
|
125
|
+
│ ├── error.middleware.js → Global error handler
|
|
126
|
+
│ ├── ip.middleware.js → IP allowlist guard
|
|
127
|
+
│ ├── rateLimit.middleware.js → Request throttling
|
|
128
|
+
│ └── encrypt.middleware.js → Placeholder for payload encryption
|
|
129
|
+
│
|
|
130
|
+
├── validations/
|
|
131
|
+
│ └── sample.validation.js → Request body/param/query validators
|
|
132
|
+
│
|
|
133
|
+
├── db/
|
|
134
|
+
│ ├── mongo.db.js → MongoDB connection setup
|
|
135
|
+
│ ├── mysql.db.js → MySQL connection setup
|
|
136
|
+
│ └── postgres.db.js → PostgreSQL connection setup
|
|
137
|
+
│
|
|
138
|
+
└── utils/
|
|
139
|
+
├── asyncHandler.js → Wraps async route handlers to catch errors
|
|
140
|
+
├── response.util.js → Standardised JSON response helpers
|
|
141
|
+
├── log.util.js → Logging service (file or external)
|
|
142
|
+
├── logSanitizer.util.js → Masks/removes sensitive fields before logging
|
|
143
|
+
├── sensitiveKeys.util.js → List of field names treated as sensitive
|
|
144
|
+
└── axios.util.js → Centralised HTTP client wrapper
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## 4. Request Lifecycle (The Full Flow)
|
|
150
|
+
|
|
151
|
+
### The Layered Architecture
|
|
152
|
+
|
|
153
|
+
This project enforces a **strict one-way data flow**. Each layer has exactly one job. No layer skips another or reaches backwards.
|
|
154
|
+
|
|
155
|
+
```
|
|
156
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
157
|
+
│ CLIENT │
|
|
158
|
+
│ HTTP Request (method + headers + body) │
|
|
159
|
+
└─────────────────────────┬───────────────────────────────────┘
|
|
160
|
+
│
|
|
161
|
+
▼
|
|
162
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
163
|
+
│ GLOBAL MIDDLEWARES │
|
|
164
|
+
│ (app.js — in order) │
|
|
165
|
+
│ │
|
|
166
|
+
│ 1. rateLimitMiddleware │
|
|
167
|
+
│ → Counts requests per IP per window │
|
|
168
|
+
│ → 429 if exceeded, otherwise continues │
|
|
169
|
+
│ │
|
|
170
|
+
│ 2. ipMiddleware │
|
|
171
|
+
│ → Checks client IP against allowlist │
|
|
172
|
+
│ → 403 if not allowed, otherwise continues │
|
|
173
|
+
│ │
|
|
174
|
+
│ 3. express.json() │
|
|
175
|
+
│ → Parses raw request body into req.body │
|
|
176
|
+
│ → Unreadable body = 400 automatically │
|
|
177
|
+
└─────────────────────────┬───────────────────────────────────┘
|
|
178
|
+
│
|
|
179
|
+
▼
|
|
180
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
181
|
+
│ ROUTER │
|
|
182
|
+
│ routes/index.route.js │
|
|
183
|
+
│ │
|
|
184
|
+
│ → Matches URL prefix (e.g. /api/v1/users) │
|
|
185
|
+
│ → Delegates to the correct feature router │
|
|
186
|
+
│ → Feature router matches the rest (e.g. /:id) │
|
|
187
|
+
│ → Calls the middleware chain for that route │
|
|
188
|
+
└─────────────────────────┬───────────────────────────────────┘
|
|
189
|
+
│
|
|
190
|
+
▼
|
|
191
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
192
|
+
│ VALIDATION MIDDLEWARE │
|
|
193
|
+
│ validations/<feature>.validation.js │
|
|
194
|
+
│ │
|
|
195
|
+
│ → Reads req.params / req.body / req.query │
|
|
196
|
+
│ → Checks required fields, types, formats │
|
|
197
|
+
│ → 422 + error list if invalid │
|
|
198
|
+
│ → Calls next() if all good — does NOT touch DB │
|
|
199
|
+
└─────────────────────────┬───────────────────────────────────┘
|
|
200
|
+
│
|
|
201
|
+
▼
|
|
202
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
203
|
+
│ CONTROLLER │
|
|
204
|
+
│ controllers/<feature>.controller.js │
|
|
205
|
+
│ │
|
|
206
|
+
│ → Extracts clean inputs from req (params, body, query) │
|
|
207
|
+
│ → Calls the service with plain values (no req/res passed) │
|
|
208
|
+
│ → Receives result from service │
|
|
209
|
+
│ → Calls ResponseUtil.success() to send response │
|
|
210
|
+
│ → Wrapped in asyncHandler — errors auto-forwarded to next()│
|
|
211
|
+
└─────────────────────────┬───────────────────────────────────┘
|
|
212
|
+
│
|
|
213
|
+
▼
|
|
214
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
215
|
+
│ SERVICE │
|
|
216
|
+
│ services/<feature>.service.js │
|
|
217
|
+
│ │
|
|
218
|
+
│ → Receives plain data (id, name, body object, etc.) │
|
|
219
|
+
│ → Contains ALL business logic and decisions │
|
|
220
|
+
│ → Calls repository to get/write data │
|
|
221
|
+
│ → Calls logService() to record meaningful events │
|
|
222
|
+
│ → Throws error with error.status set for HTTP errors │
|
|
223
|
+
│ → Returns clean result data to controller │
|
|
224
|
+
└─────────────────────────┬───────────────────────────────────┘
|
|
225
|
+
│
|
|
226
|
+
▼
|
|
227
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
228
|
+
│ REPOSITORY │
|
|
229
|
+
│ repositories/<feature>.repository.js │
|
|
230
|
+
│ │
|
|
231
|
+
│ → ONLY layer that talks to external data sources │
|
|
232
|
+
│ → Uses apiCall() for external APIs │
|
|
233
|
+
│ → Uses DB model for database operations │
|
|
234
|
+
│ → Returns raw data — no logic, no transforms │
|
|
235
|
+
└─────────────────────────┬───────────────────────────────────┘
|
|
236
|
+
│
|
|
237
|
+
┌────────┴────────┐
|
|
238
|
+
▼ ▼
|
|
239
|
+
┌──────────┐ ┌──────────────┐
|
|
240
|
+
│ Database │ │ External API │
|
|
241
|
+
│ (DB layer│ │ (axios.util) │
|
|
242
|
+
│ /db/) │ └──────────────┘
|
|
243
|
+
└──────────┘
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### The Error Path
|
|
247
|
+
|
|
248
|
+
Any layer can signal a failure. Here is exactly what happens:
|
|
249
|
+
|
|
250
|
+
```
|
|
251
|
+
Service throws: errorMiddleware receives it:
|
|
252
|
+
───────────────── ──────────────────────────
|
|
253
|
+
const err = new Error("Not found") err.status === 404
|
|
254
|
+
err.status = 404 → ResponseUtil.notFound(res, err.message)
|
|
255
|
+
throw err logService("error", err.message, { stack })
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
| Who throws | How | Where it lands |
|
|
259
|
+
|---|---|---|
|
|
260
|
+
| Service | `throw error` with `.status` | `errorMiddleware` via `asyncHandler → next(err)` |
|
|
261
|
+
| Validation | `ResponseUtil.validation()` directly | Stops at validation, never reaches controller |
|
|
262
|
+
| Repository throws (DB/API error) | Bubble up via `async/await` | `errorMiddleware` via `asyncHandler → next(err)` |
|
|
263
|
+
| Unhandled crash | Any uncaught throw | `errorMiddleware` default → 500 |
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
### Concrete Traced Example — `GET /api/v1/users/42`
|
|
268
|
+
|
|
269
|
+
```
|
|
270
|
+
1. Request arrives: GET /api/v1/users/42
|
|
271
|
+
|
|
272
|
+
2. rateLimitMiddleware
|
|
273
|
+
→ IP 127.0.0.1 has made 3 requests this window. Limit is 100. ✓ Pass.
|
|
274
|
+
|
|
275
|
+
3. ipMiddleware
|
|
276
|
+
→ clientIp = "127.0.0.1". In allowedIps. ✓ Pass.
|
|
277
|
+
|
|
278
|
+
4. express.json()
|
|
279
|
+
→ No body on GET. req.body = {}. ✓ Pass.
|
|
280
|
+
|
|
281
|
+
5. Router: /api/v1 → index.route.js
|
|
282
|
+
→ Prefix "/users" matches userRoutes.
|
|
283
|
+
→ Path "/:id" matches. req.params.id = "42".
|
|
284
|
+
|
|
285
|
+
6. validateGetUser middleware
|
|
286
|
+
→ id = "42". Not empty. Not NaN. ✓ Pass. next().
|
|
287
|
+
|
|
288
|
+
7. getUserById controller
|
|
289
|
+
→ const { id } = req.params → id = "42"
|
|
290
|
+
→ calls getUserByIdService("42")
|
|
291
|
+
|
|
292
|
+
8. getUserByIdService
|
|
293
|
+
→ calls userRepository.findById("42")
|
|
294
|
+
|
|
295
|
+
9. userRepository.findById
|
|
296
|
+
→ apiCall({ method: "GET", url: "https://api.example.com/users/42" })
|
|
297
|
+
→ Returns: { id: 42, name: "Vinay", email: "vinay@example.com" }
|
|
298
|
+
|
|
299
|
+
10. Back in service
|
|
300
|
+
→ data is not null. ✓
|
|
301
|
+
→ logService("info", "User fetched", { userId: 42 })
|
|
302
|
+
└─ sanitizeLogData masks "userId" if LOG_TYPE=2, removes if LOG_TYPE=3
|
|
303
|
+
└─ Writes JSON line to logs/app-2026-02-21.log
|
|
304
|
+
→ return { id: 42, name: "Vinay", email: "vinay@example.com" }
|
|
305
|
+
|
|
306
|
+
11. Back in controller
|
|
307
|
+
→ user = { id: 42, name: "Vinay", ... }
|
|
308
|
+
→ ResponseUtil.success(res, "User fetched successfully", user)
|
|
309
|
+
|
|
310
|
+
12. Response sent to client:
|
|
311
|
+
HTTP 200
|
|
312
|
+
{
|
|
313
|
+
"success": true,
|
|
314
|
+
"statusCode": 200,
|
|
315
|
+
"message": "User fetched successfully",
|
|
316
|
+
"data": { "id": 42, "name": "Vinay", "email": "vinay@example.com" },
|
|
317
|
+
"errors": null
|
|
318
|
+
}
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
---
|
|
322
|
+
|
|
323
|
+
### What if user ID doesn't exist — the error path traced
|
|
324
|
+
|
|
325
|
+
```
|
|
326
|
+
8. userRepository.findById("999")
|
|
327
|
+
→ API returns null / DB returns null
|
|
328
|
+
|
|
329
|
+
9. Back in service:
|
|
330
|
+
→ data is null
|
|
331
|
+
→ const err = new Error("User not found")
|
|
332
|
+
→ err.status = 404
|
|
333
|
+
→ throw err
|
|
334
|
+
|
|
335
|
+
10. asyncHandler catches the throw
|
|
336
|
+
→ calls next(err)
|
|
337
|
+
|
|
338
|
+
11. errorMiddleware receives err
|
|
339
|
+
→ logService("error", "User not found", { stack: "..." })
|
|
340
|
+
→ err.status === 404 → ResponseUtil.notFound(res, "User not found")
|
|
341
|
+
|
|
342
|
+
12. Response sent to client:
|
|
343
|
+
HTTP 404
|
|
344
|
+
{
|
|
345
|
+
"success": false,
|
|
346
|
+
"statusCode": 404,
|
|
347
|
+
"message": "User not found",
|
|
348
|
+
"data": null,
|
|
349
|
+
"errors": null
|
|
350
|
+
}
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
---
|
|
354
|
+
|
|
355
|
+
## 5. Configuration
|
|
356
|
+
|
|
357
|
+
All configuration lives in `src/config/app.config.js`. **Never read `process.env` directly anywhere else in the codebase.** Always import from config.
|
|
358
|
+
|
|
359
|
+
```js
|
|
360
|
+
// src/config/app.config.js
|
|
361
|
+
import dotenv from "dotenv";
|
|
362
|
+
dotenv.config();
|
|
363
|
+
|
|
364
|
+
export default {
|
|
365
|
+
port: process.env.PORT || 3000,
|
|
366
|
+
|
|
367
|
+
api: {
|
|
368
|
+
timeout: Number(process.env.API_TIMEOUT) || 5000,
|
|
369
|
+
},
|
|
370
|
+
|
|
371
|
+
rateLimit: {
|
|
372
|
+
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
373
|
+
max: 100, // max requests per window per IP
|
|
374
|
+
},
|
|
375
|
+
|
|
376
|
+
logging: {
|
|
377
|
+
mode: process.env.LOG_MODE || "internal", // "internal" | "external"
|
|
378
|
+
externalUrl: process.env.LOG_SERVICE_URL || "",
|
|
379
|
+
directory: "logs",
|
|
380
|
+
fileName: "app",
|
|
381
|
+
maxSize: 5 * 1024 * 1024, // 5 MB per log file
|
|
382
|
+
dailyRotate: true,
|
|
383
|
+
logType: Number(process.env.LOG_TYPE) || 1 // 1=show | 2=mask | 3=remove sensitive
|
|
384
|
+
},
|
|
385
|
+
};
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
**To add a new config value:**
|
|
389
|
+
```js
|
|
390
|
+
// .env
|
|
391
|
+
DB_URI=mongodb://localhost:27017/mydb
|
|
392
|
+
|
|
393
|
+
// app.config.js — add inside the export
|
|
394
|
+
db: {
|
|
395
|
+
uri: process.env.DB_URI || "mongodb://localhost:27017/mydb",
|
|
396
|
+
},
|
|
397
|
+
|
|
398
|
+
// Usage anywhere
|
|
399
|
+
import config from "../config/app.config.js";
|
|
400
|
+
const uri = config.db.uri;
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
---
|
|
404
|
+
|
|
405
|
+
## 6. How to Write a Feature — Step by Step
|
|
406
|
+
|
|
407
|
+
Example feature: **Users — Get user by ID**
|
|
408
|
+
|
|
409
|
+
---
|
|
410
|
+
|
|
411
|
+
### Step 1: Create the Route
|
|
412
|
+
|
|
413
|
+
```js
|
|
414
|
+
// src/routes/user.route.js
|
|
415
|
+
import express from "express";
|
|
416
|
+
import { getUserById } from "../controllers/user.controller.js";
|
|
417
|
+
import { validateGetUser } from "../validations/user.validation.js";
|
|
418
|
+
|
|
419
|
+
const router = express.Router();
|
|
420
|
+
|
|
421
|
+
router.get("/:id", validateGetUser, getUserById);
|
|
422
|
+
|
|
423
|
+
export default router;
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
**Rules:**
|
|
427
|
+
- One file per feature/resource.
|
|
428
|
+
- Route file only defines HTTP method + path + middleware chain + controller.
|
|
429
|
+
- No business logic here.
|
|
430
|
+
|
|
431
|
+
---
|
|
432
|
+
|
|
433
|
+
### Step 2: Create the Controller
|
|
434
|
+
|
|
435
|
+
```js
|
|
436
|
+
// src/controllers/user.controller.js
|
|
437
|
+
import { asyncHandler } from "../utils/asyncHandler.js";
|
|
438
|
+
import { ResponseUtil } from "../utils/response.util.js";
|
|
439
|
+
import { getUserByIdService } from "../services/user.service.js";
|
|
440
|
+
|
|
441
|
+
export const getUserById = asyncHandler(async (req, res) => {
|
|
442
|
+
const { id } = req.params;
|
|
443
|
+
|
|
444
|
+
const user = await getUserByIdService(id);
|
|
445
|
+
|
|
446
|
+
return ResponseUtil.success(res, "User fetched successfully", user);
|
|
447
|
+
});
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
**Rules:**
|
|
451
|
+
- Always wrap with `asyncHandler` — it catches all thrown errors automatically.
|
|
452
|
+
- Only extract data from `req` (params, body, query, headers) here.
|
|
453
|
+
- Never write business logic or DB queries in a controller.
|
|
454
|
+
- Always use `ResponseUtil` for the response — never call `res.json()` directly.
|
|
455
|
+
- Return the `ResponseUtil` call so the function exits cleanly.
|
|
456
|
+
|
|
457
|
+
---
|
|
458
|
+
|
|
459
|
+
### Step 3: Create Validation Middleware
|
|
460
|
+
|
|
461
|
+
```js
|
|
462
|
+
// src/validations/user.validation.js
|
|
463
|
+
import { ResponseUtil } from "../utils/response.util.js";
|
|
464
|
+
|
|
465
|
+
export const validateGetUser = (req, res, next) => {
|
|
466
|
+
const { id } = req.params;
|
|
467
|
+
|
|
468
|
+
const errors = [];
|
|
469
|
+
|
|
470
|
+
if (!id) errors.push("id is required");
|
|
471
|
+
if (isNaN(Number(id))) errors.push("id must be a number");
|
|
472
|
+
|
|
473
|
+
if (errors.length > 0) {
|
|
474
|
+
return ResponseUtil.validation(res, "Validation failed", errors);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
next();
|
|
478
|
+
};
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
**Rules:**
|
|
482
|
+
- Validation middleware runs before the controller.
|
|
483
|
+
- Use `ResponseUtil.validation()` (422) to return errors.
|
|
484
|
+
- Never throw from validation — call `ResponseUtil` directly and return.
|
|
485
|
+
- Keep validation pure: only checks shape/format of input, no DB calls.
|
|
486
|
+
|
|
487
|
+
---
|
|
488
|
+
|
|
489
|
+
### Step 4: Create the Service
|
|
490
|
+
|
|
491
|
+
```js
|
|
492
|
+
// src/services/user.service.js
|
|
493
|
+
import { userRepository } from "../repositories/user.repository.js";
|
|
494
|
+
import { logService } from "../utils/log.util.js";
|
|
495
|
+
|
|
496
|
+
export const getUserByIdService = async (id) => {
|
|
497
|
+
const user = await userRepository.findById(id);
|
|
498
|
+
|
|
499
|
+
await logService("info", "User fetched", { userId: id });
|
|
500
|
+
|
|
501
|
+
if (!user) {
|
|
502
|
+
const error = new Error("User not found");
|
|
503
|
+
error.status = 404;
|
|
504
|
+
throw error;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
return user;
|
|
508
|
+
};
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
**Rules:**
|
|
512
|
+
- Services contain all business logic.
|
|
513
|
+
- Services call repositories, never call DB drivers directly.
|
|
514
|
+
- To signal an error to the client, create an `Error`, set `error.status` to the HTTP code, and `throw` it. The `errorMiddleware` will catch it.
|
|
515
|
+
- Log meaningful events here with `logService`.
|
|
516
|
+
- Services do not touch `req` or `res`. They receive plain data and return plain data.
|
|
517
|
+
|
|
518
|
+
**Supported error status codes:**
|
|
519
|
+
|
|
520
|
+
| `error.status` | Response sent |
|
|
521
|
+
|----------------|--------------|
|
|
522
|
+
| 400 | Bad Request |
|
|
523
|
+
| 401 | Unauthorized |
|
|
524
|
+
| 403 | Forbidden |
|
|
525
|
+
| 404 | Not Found |
|
|
526
|
+
| 422 | Validation Error |
|
|
527
|
+
| anything else | 500 Internal Server Error |
|
|
528
|
+
|
|
529
|
+
---
|
|
530
|
+
|
|
531
|
+
### Step 5: Create the Repository
|
|
532
|
+
|
|
533
|
+
```js
|
|
534
|
+
// src/repositories/user.repository.js
|
|
535
|
+
import { apiCall } from "../utils/axios.util.js";
|
|
536
|
+
// OR import your DB model here
|
|
537
|
+
|
|
538
|
+
export const userRepository = {
|
|
539
|
+
findById: async (id) => {
|
|
540
|
+
// External API example:
|
|
541
|
+
return apiCall({
|
|
542
|
+
method: "GET",
|
|
543
|
+
url: `https://jsonplaceholder.typicode.com/users/${id}`,
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
// MongoDB example (once model is connected):
|
|
547
|
+
// return UserModel.findById(id).lean();
|
|
548
|
+
|
|
549
|
+
// MySQL/Postgres example:
|
|
550
|
+
// return db.query("SELECT * FROM users WHERE id = ?", [id]);
|
|
551
|
+
},
|
|
552
|
+
};
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
**Rules:**
|
|
556
|
+
- Repositories are the only layer allowed to talk to a DB or external API.
|
|
557
|
+
- Export a plain object with named methods — this makes it easy to mock in tests.
|
|
558
|
+
- Never put business logic here. Just raw data operations.
|
|
559
|
+
- Return raw data. Let the service decide what to do with it.
|
|
560
|
+
|
|
561
|
+
---
|
|
562
|
+
|
|
563
|
+
### Step 6: Create the Model (optional)
|
|
564
|
+
|
|
565
|
+
Models are used only when you have a connected database. Leave empty if using only external API calls.
|
|
566
|
+
|
|
567
|
+
```js
|
|
568
|
+
// src/models/user.model.js — MongoDB example
|
|
569
|
+
import mongoose from "mongoose";
|
|
570
|
+
|
|
571
|
+
const userSchema = new mongoose.Schema({
|
|
572
|
+
name: { type: String, required: true },
|
|
573
|
+
email: { type: String, required: true, unique: true },
|
|
574
|
+
createdAt: { type: Date, default: Date.now },
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
export const UserModel = mongoose.model("User", userSchema);
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
---
|
|
581
|
+
|
|
582
|
+
### Step 7: Register the Route
|
|
583
|
+
|
|
584
|
+
```js
|
|
585
|
+
// src/routes/index.route.js
|
|
586
|
+
import express from "express";
|
|
587
|
+
import sampleRoutes from "./sample1.route.js";
|
|
588
|
+
import userRoutes from "./user.route.js"; // ← add this
|
|
589
|
+
|
|
590
|
+
const router = express.Router();
|
|
591
|
+
|
|
592
|
+
router.use("/sample", sampleRoutes);
|
|
593
|
+
router.use("/users", userRoutes); // ← and this
|
|
594
|
+
|
|
595
|
+
export default router;
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
Final endpoint: `GET /api/v1/users/:id`
|
|
599
|
+
|
|
600
|
+
---
|
|
601
|
+
|
|
602
|
+
## 7. Utilities — How to Use Each One
|
|
603
|
+
|
|
604
|
+
---
|
|
605
|
+
|
|
606
|
+
### asyncHandler
|
|
607
|
+
|
|
608
|
+
**File:** `src/utils/asyncHandler.js`
|
|
609
|
+
|
|
610
|
+
**Purpose:** Eliminates the need for `try/catch` in every controller. If the async function throws, it automatically calls `next(error)` which triggers `errorMiddleware`.
|
|
611
|
+
|
|
612
|
+
```js
|
|
613
|
+
// Without asyncHandler — verbose and error-prone
|
|
614
|
+
router.get("/", async (req, res, next) => {
|
|
615
|
+
try {
|
|
616
|
+
const data = await someService();
|
|
617
|
+
res.json(data);
|
|
618
|
+
} catch (err) {
|
|
619
|
+
next(err);
|
|
620
|
+
}
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
// With asyncHandler — clean
|
|
624
|
+
import { asyncHandler } from "../utils/asyncHandler.js";
|
|
625
|
+
|
|
626
|
+
router.get("/", asyncHandler(async (req, res) => {
|
|
627
|
+
const data = await someService();
|
|
628
|
+
ResponseUtil.success(res, "OK", data);
|
|
629
|
+
}));
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
**Rule:** Every controller function must be wrapped in `asyncHandler`. No exceptions.
|
|
633
|
+
|
|
634
|
+
---
|
|
635
|
+
|
|
636
|
+
### ResponseUtil
|
|
637
|
+
|
|
638
|
+
**File:** `src/utils/response.util.js`
|
|
639
|
+
|
|
640
|
+
**Purpose:** Enforces a consistent JSON response shape across the entire API.
|
|
641
|
+
|
|
642
|
+
**Response shape:**
|
|
643
|
+
```json
|
|
644
|
+
{
|
|
645
|
+
"success": true,
|
|
646
|
+
"statusCode": 200,
|
|
647
|
+
"message": "User fetched successfully",
|
|
648
|
+
"data": { ... },
|
|
649
|
+
"errors": null
|
|
650
|
+
}
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
**Available methods:**
|
|
654
|
+
|
|
655
|
+
```js
|
|
656
|
+
import { ResponseUtil } from "../utils/response.util.js";
|
|
657
|
+
|
|
658
|
+
ResponseUtil.success(res, "Message", data); // 200
|
|
659
|
+
ResponseUtil.created(res, "Created", data); // 201
|
|
660
|
+
ResponseUtil.badRequest(res, "Bad input", errors); // 400
|
|
661
|
+
ResponseUtil.unauthorized(res, "Login required"); // 401
|
|
662
|
+
ResponseUtil.forbidden(res, "No access"); // 403
|
|
663
|
+
ResponseUtil.notFound(res, "Not found"); // 404
|
|
664
|
+
ResponseUtil.validation(res, "Failed", errorsArray); // 422
|
|
665
|
+
ResponseUtil.exception(res, "Server error"); // 500
|
|
666
|
+
|
|
667
|
+
// Custom status code:
|
|
668
|
+
ResponseUtil.send(res, 429, "Too many requests");
|
|
669
|
+
```
|
|
670
|
+
|
|
671
|
+
**Rule:** Never use `res.json()`, `res.send()`, or `res.status()` directly. Always go through `ResponseUtil`.
|
|
672
|
+
|
|
673
|
+
---
|
|
674
|
+
|
|
675
|
+
### logService
|
|
676
|
+
|
|
677
|
+
**File:** `src/utils/log.util.js`
|
|
678
|
+
|
|
679
|
+
**Purpose:** Writes structured log entries to a rotating daily log file, or posts them to an external log service.
|
|
680
|
+
|
|
681
|
+
```js
|
|
682
|
+
import { logService } from "../utils/log.util.js";
|
|
683
|
+
|
|
684
|
+
// Levels: "info" | "warn" | "error" | "debug"
|
|
685
|
+
await logService("info", "User logged in", { userId: 42, role: "admin" });
|
|
686
|
+
await logService("error", "DB connection failed", { host: "localhost" });
|
|
687
|
+
await logService("warn", "Rate limit approaching", { ip: "192.168.1.1" });
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
**Log output (written to `logs/app-YYYY-MM-DD.log`):**
|
|
691
|
+
```json
|
|
692
|
+
{
|
|
693
|
+
"level": "info",
|
|
694
|
+
"message": "User logged in",
|
|
695
|
+
"timestamp": "21/02/2026, 10:30:00 am",
|
|
696
|
+
"meta": { "userId": 42, "role": "admin" }
|
|
697
|
+
}
|
|
698
|
+
```
|
|
699
|
+
|
|
700
|
+
**Rules:**
|
|
701
|
+
- Always `await` the log call.
|
|
702
|
+
- Log at the service layer, not in controllers or repositories.
|
|
703
|
+
- Sensitive fields in `meta` are automatically masked based on `LOG_TYPE` (see section 10).
|
|
704
|
+
|
|
705
|
+
---
|
|
706
|
+
|
|
707
|
+
### apiCall (axios.util)
|
|
708
|
+
|
|
709
|
+
**File:** `src/utils/axios.util.js`
|
|
710
|
+
|
|
711
|
+
**Purpose:** A centralised HTTP client wrapper around axios with a global timeout from config.
|
|
712
|
+
|
|
713
|
+
```js
|
|
714
|
+
import { apiCall } from "../utils/axios.util.js";
|
|
715
|
+
|
|
716
|
+
// GET request
|
|
717
|
+
const posts = await apiCall({
|
|
718
|
+
method: "GET",
|
|
719
|
+
url: "https://api.example.com/posts",
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
// POST request with body
|
|
723
|
+
const result = await apiCall({
|
|
724
|
+
method: "POST",
|
|
725
|
+
url: "https://api.example.com/users",
|
|
726
|
+
data: { name: "Vinay", email: "vinay@example.com" },
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
// With custom headers
|
|
730
|
+
const secured = await apiCall({
|
|
731
|
+
method: "GET",
|
|
732
|
+
url: "https://api.example.com/secure",
|
|
733
|
+
headers: { Authorization: "Bearer TOKEN" },
|
|
734
|
+
});
|
|
735
|
+
```
|
|
736
|
+
|
|
737
|
+
**Rules:**
|
|
738
|
+
- Only use `apiCall` from repositories when calling external APIs.
|
|
739
|
+
- Never call `axios` directly anywhere in the codebase.
|
|
740
|
+
- Timeout is set globally via `config.api.timeout` (default 5000ms).
|
|
741
|
+
|
|
742
|
+
---
|
|
743
|
+
|
|
744
|
+
### logSanitizer / sensitiveKeys
|
|
745
|
+
|
|
746
|
+
**Files:** `src/utils/logSanitizer.util.js`, `src/utils/sensitiveKeys.util.js`
|
|
747
|
+
|
|
748
|
+
**Purpose:** Automatically strips or masks sensitive field values from log metadata so passwords, tokens, and IDs are never written to log files in plain text.
|
|
749
|
+
|
|
750
|
+
**To add a sensitive field:**
|
|
751
|
+
```js
|
|
752
|
+
// src/utils/sensitiveKeys.util.js
|
|
753
|
+
export const sensitiveKeys = [
|
|
754
|
+
"userId", // existing
|
|
755
|
+
"id", // existing
|
|
756
|
+
"password", // ← add yours here
|
|
757
|
+
"token",
|
|
758
|
+
"email",
|
|
759
|
+
"creditCard",
|
|
760
|
+
];
|
|
761
|
+
```
|
|
762
|
+
|
|
763
|
+
**Behaviour controlled by `LOG_TYPE` in `.env`:**
|
|
764
|
+
|
|
765
|
+
| `LOG_TYPE` | Behaviour |
|
|
766
|
+
|-----------|-----------|
|
|
767
|
+
| `1` | Sensitive values shown as-is (development) |
|
|
768
|
+
| `2` | Sensitive values masked: `vi****ay` |
|
|
769
|
+
| `3` | Sensitive keys removed entirely from logs |
|
|
770
|
+
|
|
771
|
+
**You never need to call `sanitizeLogData` manually** — `logService` calls it automatically.
|
|
772
|
+
|
|
773
|
+
---
|
|
774
|
+
|
|
775
|
+
## 8. Middlewares
|
|
776
|
+
|
|
777
|
+
---
|
|
778
|
+
|
|
779
|
+
### rateLimitMiddleware
|
|
780
|
+
|
|
781
|
+
**File:** `src/middlewares/rateLimit.middleware.js`
|
|
782
|
+
|
|
783
|
+
Limits each IP to **100 requests per 15-minute window** (configurable in `app.config.js`).
|
|
784
|
+
|
|
785
|
+
Returns `429 Too Many Requests` via `ResponseUtil.send` when exceeded.
|
|
786
|
+
|
|
787
|
+
**To change limits:**
|
|
788
|
+
```js
|
|
789
|
+
// app.config.js
|
|
790
|
+
rateLimit: {
|
|
791
|
+
windowMs: 10 * 60 * 1000, // 10 minutes
|
|
792
|
+
max: 50, // 50 requests
|
|
793
|
+
},
|
|
794
|
+
```
|
|
795
|
+
|
|
796
|
+
---
|
|
797
|
+
|
|
798
|
+
### ipMiddleware
|
|
799
|
+
|
|
800
|
+
**File:** `src/middlewares/ip.middleware.js`
|
|
801
|
+
|
|
802
|
+
Only allows requests from IPs listed in the `allowedIps` array. All others receive `403 Forbidden`.
|
|
803
|
+
|
|
804
|
+
**To allow more IPs:**
|
|
805
|
+
```js
|
|
806
|
+
const allowedIps = [
|
|
807
|
+
"127.0.0.1", // localhost IPv4
|
|
808
|
+
"::1", // localhost IPv6
|
|
809
|
+
"192.168.1.10", // existing
|
|
810
|
+
"203.0.113.50", // ← add your server/office IP
|
|
811
|
+
];
|
|
812
|
+
```
|
|
813
|
+
|
|
814
|
+
> To disable IP filtering in development, move `ipMiddleware` out of `app.js` or wrap it with an env check.
|
|
815
|
+
|
|
816
|
+
---
|
|
817
|
+
|
|
818
|
+
### errorMiddleware
|
|
819
|
+
|
|
820
|
+
**File:** `src/middlewares/error.middleware.js`
|
|
821
|
+
|
|
822
|
+
The **last middleware** in `app.js`. Catches every error that reaches `next(err)` (including those thrown inside `asyncHandler`).
|
|
823
|
+
|
|
824
|
+
- Logs the error with `logService("error", ...)`.
|
|
825
|
+
- Maps `error.status` to the correct `ResponseUtil` method.
|
|
826
|
+
- You never need to call this manually — just throw errors in services.
|
|
827
|
+
|
|
828
|
+
**How to trigger it from a service:**
|
|
829
|
+
```js
|
|
830
|
+
const error = new Error("Item not found");
|
|
831
|
+
error.status = 404;
|
|
832
|
+
throw error; // ← asyncHandler sends this to errorMiddleware
|
|
833
|
+
```
|
|
834
|
+
|
|
835
|
+
---
|
|
836
|
+
|
|
837
|
+
### encryptMiddleware
|
|
838
|
+
|
|
839
|
+
**File:** `src/middlewares/encrypt.middleware.js`
|
|
840
|
+
|
|
841
|
+
A placeholder for encrypting/decrypting request or response payloads. Currently a no-op (`next()` only).
|
|
842
|
+
|
|
843
|
+
**To use it on specific routes:**
|
|
844
|
+
```js
|
|
845
|
+
import { encryptMiddleware } from "../middlewares/encrypt.middleware.js";
|
|
846
|
+
|
|
847
|
+
router.post("/sensitive-data", encryptMiddleware, myController);
|
|
848
|
+
```
|
|
849
|
+
|
|
850
|
+
**To add to all routes**, add it in `app.js`:
|
|
851
|
+
```js
|
|
852
|
+
app.use(encryptMiddleware);
|
|
853
|
+
```
|
|
854
|
+
|
|
855
|
+
---
|
|
856
|
+
|
|
857
|
+
## 9. Database Connections
|
|
858
|
+
|
|
859
|
+
The `src/db/` folder has three empty setup files. Choose the one matching your database, connect in `server.js` before starting the listener.
|
|
860
|
+
|
|
861
|
+
### MongoDB (Mongoose)
|
|
862
|
+
|
|
863
|
+
```js
|
|
864
|
+
// src/db/mongo.db.js
|
|
865
|
+
import mongoose from "mongoose";
|
|
866
|
+
import config from "../config/app.config.js";
|
|
867
|
+
|
|
868
|
+
export const connectMongo = async () => {
|
|
869
|
+
await mongoose.connect(config.db.uri);
|
|
870
|
+
console.log("MongoDB connected");
|
|
871
|
+
};
|
|
872
|
+
```
|
|
873
|
+
|
|
874
|
+
```js
|
|
875
|
+
// src/server.js
|
|
876
|
+
import { connectMongo } from "./db/mongo.db.js";
|
|
877
|
+
import app from "./app.js";
|
|
878
|
+
import config from "./config/app.config.js";
|
|
879
|
+
|
|
880
|
+
connectMongo().then(() => {
|
|
881
|
+
app.listen(config.port, () => {
|
|
882
|
+
console.log(`Server running on port: ${config.port}`);
|
|
883
|
+
});
|
|
884
|
+
});
|
|
885
|
+
```
|
|
886
|
+
|
|
887
|
+
### MySQL / PostgreSQL
|
|
888
|
+
|
|
889
|
+
Follow the same pattern using `mysql2` or `pg` pool, exporting a `connectDB` function and calling it in `server.js`.
|
|
890
|
+
|
|
891
|
+
---
|
|
892
|
+
|
|
893
|
+
## 10. Logging System Deep Dive
|
|
894
|
+
|
|
895
|
+
| Setting | Controlled by | Default |
|
|
896
|
+
|---------|--------------|---------|
|
|
897
|
+
| Log destination | `LOG_MODE` env | `internal` (file) |
|
|
898
|
+
| External log URL | `LOG_SERVICE_URL` env | none |
|
|
899
|
+
| Log file name | `app.config.js` | `app-YYYY-MM-DD.log` |
|
|
900
|
+
| Max file size | `app.config.js` | 5 MB (then `.backup`) |
|
|
901
|
+
| Daily rotation | `app.config.js` | enabled |
|
|
902
|
+
| Sensitive data | `LOG_TYPE` env | `1` (show all) |
|
|
903
|
+
|
|
904
|
+
**In development**, set `LOG_TYPE=1` to see all data.
|
|
905
|
+
**In production**, set `LOG_TYPE=3` to remove all sensitive fields from logs.
|
|
906
|
+
|
|
907
|
+
Log files are written to the `logs/` directory at the project root.
|
|
908
|
+
|
|
909
|
+
---
|
|
910
|
+
|
|
911
|
+
## 11. Environment Variables Reference
|
|
912
|
+
|
|
913
|
+
Create a `.env` file in the project root:
|
|
914
|
+
|
|
915
|
+
```env
|
|
916
|
+
# Server
|
|
917
|
+
PORT=3000
|
|
918
|
+
|
|
919
|
+
# API
|
|
920
|
+
API_TIMEOUT=5000
|
|
921
|
+
|
|
922
|
+
# Logging
|
|
923
|
+
LOG_MODE=internal # internal | external
|
|
924
|
+
LOG_SERVICE_URL= # only needed if LOG_MODE=external
|
|
925
|
+
LOG_TYPE=1 # 1=show | 2=mask | 3=remove sensitive fields
|
|
926
|
+
|
|
927
|
+
# Database (add as needed)
|
|
928
|
+
DB_URI=mongodb://localhost:27017/mydb
|
|
929
|
+
```
|
|
930
|
+
|
|
931
|
+
---
|
|
932
|
+
|
|
933
|
+
## 12. Code Standards & Conventions
|
|
934
|
+
|
|
935
|
+
> These are not suggestions. They are the rules that keep the codebase consistent, predictable, and easy to navigate for anyone who picks it up.
|
|
936
|
+
|
|
937
|
+
---
|
|
938
|
+
|
|
939
|
+
### Rule 1 — Single Responsibility Per Layer
|
|
940
|
+
|
|
941
|
+
Every file has exactly one job. If you find yourself doing two things in one file, split it.
|
|
942
|
+
|
|
943
|
+
| Layer | Its ONE job | What it must NOT do |
|
|
944
|
+
|-------|------------|---------------------|
|
|
945
|
+
| Route | Define URL shape + middleware chain | No logic, no DB calls |
|
|
946
|
+
| Validation | Check input shape/format | No DB calls, no business rules |
|
|
947
|
+
| Controller | Bridge req → service → res | No business logic, no DB calls |
|
|
948
|
+
| Service | Business logic + decisions | No res.json(), no DB calls directly |
|
|
949
|
+
| Repository | Read/write data | No business rules, no response building |
|
|
950
|
+
| Util | A single reusable operation | No app-specific logic |
|
|
951
|
+
| Middleware | One cross-cutting concern | No feature-specific business logic |
|
|
952
|
+
|
|
953
|
+
---
|
|
954
|
+
|
|
955
|
+
### Rule 2 — File & Folder Naming
|
|
956
|
+
|
|
957
|
+
```
|
|
958
|
+
✅ user.route.js → <feature>.route.js
|
|
959
|
+
✅ user.controller.js → <feature>.controller.js
|
|
960
|
+
✅ user.service.js → <feature>.service.js
|
|
961
|
+
✅ user.repository.js → <feature>.repository.js
|
|
962
|
+
✅ user.model.js → <feature>.model.js
|
|
963
|
+
✅ user.validation.js → <feature>.validation.js
|
|
964
|
+
✅ auth.middleware.js → <name>.middleware.js
|
|
965
|
+
✅ response.util.js → <name>.util.js
|
|
966
|
+
|
|
967
|
+
❌ userController.js (no camelCase file names)
|
|
968
|
+
❌ UserRoute.js (no PascalCase file names)
|
|
969
|
+
❌ get-user.js (no verb-based names for modules)
|
|
970
|
+
```
|
|
971
|
+
|
|
972
|
+
- Use `kebab-case` for all file names.
|
|
973
|
+
- Never abbreviate (`usr`, `ctrl`, `svc`) — spell it out.
|
|
974
|
+
- Group files in the folder that matches their layer. Never mix layers in one folder.
|
|
975
|
+
|
|
976
|
+
---
|
|
977
|
+
|
|
978
|
+
### Rule 3 — Naming Conventions in Code
|
|
979
|
+
|
|
980
|
+
```js
|
|
981
|
+
// ✅ Variables & functions → camelCase
|
|
982
|
+
const userId = req.params.id;
|
|
983
|
+
const fetchedUser = await getUserByIdService(id);
|
|
984
|
+
|
|
985
|
+
// ✅ Classes & Models → PascalCase
|
|
986
|
+
class UserRepository { ... }
|
|
987
|
+
const UserModel = mongoose.model("User", userSchema);
|
|
988
|
+
|
|
989
|
+
// ✅ Constants that never change → UPPER_SNAKE_CASE
|
|
990
|
+
const MAX_RETRIES = 3;
|
|
991
|
+
const DEFAULT_TIMEOUT = 5000;
|
|
992
|
+
|
|
993
|
+
// ✅ Exported service functions → camelCase verb + noun
|
|
994
|
+
export const getUserByIdService = async (id) => { ... };
|
|
995
|
+
export const createUserService = async (data) => { ... };
|
|
996
|
+
export const deleteUserService = async (id) => { ... };
|
|
997
|
+
|
|
998
|
+
// ✅ Exported controllers → camelCase verb + noun
|
|
999
|
+
export const getUser = asyncHandler(async (req, res) => { ... });
|
|
1000
|
+
export const createUser = asyncHandler(async (req, res) => { ... });
|
|
1001
|
+
export const deleteUser = asyncHandler(async (req, res) => { ... });
|
|
1002
|
+
|
|
1003
|
+
// ✅ Validation exports → validate + PascalCase feature + action
|
|
1004
|
+
export const validateGetUser = (req, res, next) => { ... };
|
|
1005
|
+
export const validateCreateUser = (req, res, next) => { ... };
|
|
1006
|
+
|
|
1007
|
+
// ❌ Avoid vague names
|
|
1008
|
+
const data = ...; // what data?
|
|
1009
|
+
const result = ...; // result of what?
|
|
1010
|
+
const temp = ...; // always temporary — never commit this
|
|
1011
|
+
const x = ...; // never
|
|
1012
|
+
```
|
|
1013
|
+
|
|
1014
|
+
---
|
|
1015
|
+
|
|
1016
|
+
### Rule 4 — Import Order
|
|
1017
|
+
|
|
1018
|
+
Organise imports in this order, with a blank line between groups:
|
|
1019
|
+
|
|
1020
|
+
```js
|
|
1021
|
+
// 1. Node.js built-in modules
|
|
1022
|
+
import fs from "fs";
|
|
1023
|
+
import path from "path";
|
|
1024
|
+
|
|
1025
|
+
// 2. Third-party npm packages
|
|
1026
|
+
import express from "express";
|
|
1027
|
+
import axios from "axios";
|
|
1028
|
+
|
|
1029
|
+
// 3. Internal config
|
|
1030
|
+
import config from "../config/app.config.js";
|
|
1031
|
+
|
|
1032
|
+
// 4. Internal utils
|
|
1033
|
+
import { logService } from "../utils/log.util.js";
|
|
1034
|
+
import { ResponseUtil } from "../utils/response.util.js";
|
|
1035
|
+
|
|
1036
|
+
// 5. Internal app modules (routes, controllers, services, repos, models)
|
|
1037
|
+
import { userRepository } from "../repositories/user.repository.js";
|
|
1038
|
+
```
|
|
1039
|
+
|
|
1040
|
+
- Always include the `.js` extension — ES Modules require it.
|
|
1041
|
+
- Always use relative paths within `src/`.
|
|
1042
|
+
- Never use `index.js` barrel re-exports — import the file directly.
|
|
1043
|
+
|
|
1044
|
+
---
|
|
1045
|
+
|
|
1046
|
+
### Rule 5 — Exports
|
|
1047
|
+
|
|
1048
|
+
```js
|
|
1049
|
+
// Controllers → named export per handler
|
|
1050
|
+
export const getUser = asyncHandler(...);
|
|
1051
|
+
export const createUser = asyncHandler(...);
|
|
1052
|
+
|
|
1053
|
+
// Services → named export per function
|
|
1054
|
+
export const getUserByIdService = async (id) => { ... };
|
|
1055
|
+
|
|
1056
|
+
// Repositories → named export of a single object
|
|
1057
|
+
export const userRepository = {
|
|
1058
|
+
findById: async (id) => { ... },
|
|
1059
|
+
create: async (data) => { ... },
|
|
1060
|
+
};
|
|
1061
|
+
|
|
1062
|
+
// Config → single default export
|
|
1063
|
+
export default { port: ..., db: { ... } };
|
|
1064
|
+
|
|
1065
|
+
// Utils → named exports
|
|
1066
|
+
export const ResponseUtil = { ... };
|
|
1067
|
+
export const asyncHandler = (fn) => { ... };
|
|
1068
|
+
```
|
|
1069
|
+
|
|
1070
|
+
**Never mix named and default exports in the same file.**
|
|
1071
|
+
|
|
1072
|
+
---
|
|
1073
|
+
|
|
1074
|
+
### Rule 6 — Async / Await
|
|
1075
|
+
|
|
1076
|
+
```js
|
|
1077
|
+
// ✅ Always async/await
|
|
1078
|
+
const user = await userRepository.findById(id);
|
|
1079
|
+
|
|
1080
|
+
// ✅ Controller always wrapped in asyncHandler
|
|
1081
|
+
export const getUser = asyncHandler(async (req, res) => {
|
|
1082
|
+
const user = await getUserByIdService(req.params.id);
|
|
1083
|
+
return ResponseUtil.success(res, "OK", user);
|
|
1084
|
+
});
|
|
1085
|
+
|
|
1086
|
+
// ❌ Never raw .then().catch() chains
|
|
1087
|
+
userRepository.findById(id)
|
|
1088
|
+
.then(user => res.json(user))
|
|
1089
|
+
.catch(err => next(err)); // ← don't do this
|
|
1090
|
+
|
|
1091
|
+
// ❌ Never forget await — silent bugs
|
|
1092
|
+
const user = userRepository.findById(id); // returns a Promise, not data!
|
|
1093
|
+
```
|
|
1094
|
+
|
|
1095
|
+
---
|
|
1096
|
+
|
|
1097
|
+
### Rule 7 — Error Handling
|
|
1098
|
+
|
|
1099
|
+
```js
|
|
1100
|
+
// ✅ In a service — throw with a status code
|
|
1101
|
+
if (!user) {
|
|
1102
|
+
const err = new Error("User not found");
|
|
1103
|
+
err.status = 404;
|
|
1104
|
+
throw err;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
// ✅ Carry extra detail when needed
|
|
1108
|
+
if (existingUser) {
|
|
1109
|
+
const err = new Error("Email already registered");
|
|
1110
|
+
err.status = 409; // Conflict
|
|
1111
|
+
err.field = "email"; // optional extra context
|
|
1112
|
+
throw err;
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
// ✅ In validation middleware — respond directly, don't throw
|
|
1116
|
+
if (errors.length > 0) {
|
|
1117
|
+
return ResponseUtil.validation(res, "Validation failed", errors);
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
// ❌ Never swallow errors silently
|
|
1121
|
+
try {
|
|
1122
|
+
await doSomething();
|
|
1123
|
+
} catch (err) {
|
|
1124
|
+
// nothing here — the error disappears and the bug is hidden
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
// ❌ Never send response from a service
|
|
1128
|
+
if (!user) {
|
|
1129
|
+
return res.status(404).json({ message: "Not found" }); // ← wrong layer
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
// ❌ Never catch-and-ignore in repositories
|
|
1133
|
+
findById: async (id) => {
|
|
1134
|
+
try {
|
|
1135
|
+
return await UserModel.findById(id);
|
|
1136
|
+
} catch (err) {
|
|
1137
|
+
return null; // ← bug hidden, service thinks "not found" when it's a crash
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
```
|
|
1141
|
+
|
|
1142
|
+
**HTTP status codes to use:**
|
|
1143
|
+
|
|
1144
|
+
| Code | Meaning | When to use |
|
|
1145
|
+
|------|---------|-------------|
|
|
1146
|
+
| `400` | Bad Request | Malformed input the client sent |
|
|
1147
|
+
| `401` | Unauthorized | Not logged in / missing token |
|
|
1148
|
+
| `403` | Forbidden | Logged in but no permission |
|
|
1149
|
+
| `404` | Not Found | Resource does not exist |
|
|
1150
|
+
| `409` | Conflict | Duplicate resource (email already exists) |
|
|
1151
|
+
| `422` | Unprocessable | Validation failed (use from validation layer) |
|
|
1152
|
+
| `429` | Too Many Requests | Rate limit exceeded (handled by middleware) |
|
|
1153
|
+
| `500` | Server Error | Anything unexpected — default fallback |
|
|
1154
|
+
|
|
1155
|
+
---
|
|
1156
|
+
|
|
1157
|
+
### Rule 8 — Responses
|
|
1158
|
+
|
|
1159
|
+
```js
|
|
1160
|
+
// ✅ Always use ResponseUtil
|
|
1161
|
+
return ResponseUtil.success(res, "User created", user); // 200
|
|
1162
|
+
return ResponseUtil.created(res, "User created", user); // 201
|
|
1163
|
+
return ResponseUtil.notFound(res, "User not found"); // 404
|
|
1164
|
+
|
|
1165
|
+
// ✅ Always return the ResponseUtil call in a controller
|
|
1166
|
+
export const getUser = asyncHandler(async (req, res) => {
|
|
1167
|
+
const user = await getUserByIdService(id);
|
|
1168
|
+
return ResponseUtil.success(res, "OK", user); // ← return prevents accidental double-send
|
|
1169
|
+
});
|
|
1170
|
+
|
|
1171
|
+
// ❌ Never call res directly
|
|
1172
|
+
res.json({ user }); // no standard shape
|
|
1173
|
+
res.status(200).send(user); // bypasses ResponseUtil
|
|
1174
|
+
res.status(201).json({ user }); // inconsistent shape
|
|
1175
|
+
```
|
|
1176
|
+
|
|
1177
|
+
**Every API response has this exact shape — always:**
|
|
1178
|
+
```json
|
|
1179
|
+
{
|
|
1180
|
+
"success": true,
|
|
1181
|
+
"statusCode": 200,
|
|
1182
|
+
"message": "User fetched successfully",
|
|
1183
|
+
"data": { },
|
|
1184
|
+
"errors": null
|
|
1185
|
+
}
|
|
1186
|
+
```
|
|
1187
|
+
|
|
1188
|
+
---
|
|
1189
|
+
|
|
1190
|
+
### Rule 9 — Logging
|
|
1191
|
+
|
|
1192
|
+
```js
|
|
1193
|
+
// ✅ Log meaningful business events in services
|
|
1194
|
+
await logService("info", "User registered", { userId: user.id, plan: "free" });
|
|
1195
|
+
await logService("warn", "Login failed", { email, attempts: failCount });
|
|
1196
|
+
await logService("error", "Payment failed", { orderId, reason });
|
|
1197
|
+
|
|
1198
|
+
// ✅ Always await logService
|
|
1199
|
+
await logService("info", "...", { ... }); // ← sync file writes need this
|
|
1200
|
+
|
|
1201
|
+
// ✅ Include relevant context in meta — not just the message
|
|
1202
|
+
await logService("info", "Order placed", { orderId, userId, amount, currency });
|
|
1203
|
+
|
|
1204
|
+
// ❌ Don't log in controllers — wrong layer
|
|
1205
|
+
export const getUser = asyncHandler(async (req, res) => {
|
|
1206
|
+
await logService("info", "getUser called"); // ← move this to the service
|
|
1207
|
+
...
|
|
1208
|
+
});
|
|
1209
|
+
|
|
1210
|
+
// ❌ Don't use console.log in production code
|
|
1211
|
+
console.log(user); // won't be in log files, not structured, not sanitized
|
|
1212
|
+
console.error(err); // use logService("error", ...) instead
|
|
1213
|
+
|
|
1214
|
+
// ❌ Don't log sensitive data manually
|
|
1215
|
+
await logService("info", "User login", { password: req.body.password }); // ← add "password" to sensitiveKeys instead
|
|
1216
|
+
```
|
|
1217
|
+
|
|
1218
|
+
**Log levels — when to use which:**
|
|
1219
|
+
|
|
1220
|
+
| Level | When |
|
|
1221
|
+
|-------|------|
|
|
1222
|
+
| `"info"` | Normal business events (user created, order placed, data fetched) |
|
|
1223
|
+
| `"warn"` | Something unexpected but not fatal (retry attempted, limit approaching) |
|
|
1224
|
+
| `"error"` | Something failed that needs attention (DB error, payment failed) |
|
|
1225
|
+
| `"debug"` | Detailed info for tracing — remove before production |
|
|
1226
|
+
|
|
1227
|
+
---
|
|
1228
|
+
|
|
1229
|
+
### Rule 10 — Configuration
|
|
1230
|
+
|
|
1231
|
+
```js
|
|
1232
|
+
// ✅ Only ever read process.env in app.config.js
|
|
1233
|
+
// app.config.js
|
|
1234
|
+
export default {
|
|
1235
|
+
db: { uri: process.env.DB_URI || "mongodb://localhost/mydb" },
|
|
1236
|
+
};
|
|
1237
|
+
|
|
1238
|
+
// ✅ Everywhere else — import from config
|
|
1239
|
+
import config from "../config/app.config.js";
|
|
1240
|
+
const uri = config.db.uri;
|
|
1241
|
+
|
|
1242
|
+
// ❌ Never read process.env outside of app.config.js
|
|
1243
|
+
const timeout = Number(process.env.API_TIMEOUT); // ← scattered, untracked
|
|
1244
|
+
if (process.env.NODE_ENV === "production") { ... } // ← add NODE_ENV to config first
|
|
1245
|
+
```
|
|
1246
|
+
|
|
1247
|
+
This means all env usage is visible in one file. If an env var is renamed or removed, there's one place to fix it.
|
|
1248
|
+
|
|
1249
|
+
---
|
|
1250
|
+
|
|
1251
|
+
### Rule 11 — Keep Functions Small and Flat
|
|
1252
|
+
|
|
1253
|
+
```js
|
|
1254
|
+
// ✅ One function = one action. Short and readable.
|
|
1255
|
+
export const createUserService = async (data) => {
|
|
1256
|
+
const exists = await userRepository.findByEmail(data.email);
|
|
1257
|
+
|
|
1258
|
+
if (exists) {
|
|
1259
|
+
const err = new Error("Email already registered");
|
|
1260
|
+
err.status = 409;
|
|
1261
|
+
throw err;
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
const user = await userRepository.create(data);
|
|
1265
|
+
await logService("info", "User created", { userId: user.id });
|
|
1266
|
+
return user;
|
|
1267
|
+
};
|
|
1268
|
+
|
|
1269
|
+
// ❌ Avoid deep nesting — flatten with early returns
|
|
1270
|
+
export const createUserService = async (data) => {
|
|
1271
|
+
const exists = await userRepository.findByEmail(data.email);
|
|
1272
|
+
if (!exists) {
|
|
1273
|
+
const user = await userRepository.create(data);
|
|
1274
|
+
if (user) {
|
|
1275
|
+
await logService("info", "created", { id: user.id });
|
|
1276
|
+
if (user.id) {
|
|
1277
|
+
return user; // ← 4 levels deep, hard to follow
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
};
|
|
1282
|
+
```
|
|
1283
|
+
|
|
1284
|
+
**Target:** max **2 levels of nesting** inside a function. Use early returns to bail out.
|
|
1285
|
+
|
|
1286
|
+
---
|
|
1287
|
+
|
|
1288
|
+
### Rule 12 — Comments
|
|
1289
|
+
|
|
1290
|
+
```js
|
|
1291
|
+
// ✅ Comment WHY, not WHAT — the code already says what
|
|
1292
|
+
// Rate window is 15 min to align with our SLA response commitment
|
|
1293
|
+
windowMs: 15 * 60 * 1000,
|
|
1294
|
+
|
|
1295
|
+
// ✅ Mark intentional decisions
|
|
1296
|
+
// LOG_TYPE 3 removes keys entirely — preferred for PCI-DSS compliance
|
|
1297
|
+
if (config.logging.logType === 3) { continue; }
|
|
1298
|
+
|
|
1299
|
+
// ❌ Don't comment obvious code
|
|
1300
|
+
// Get user by id
|
|
1301
|
+
const user = await userRepository.findById(id); // ← the code already says this
|
|
1302
|
+
|
|
1303
|
+
// ❌ Don't leave dead code commented out — delete it (git can restore it)
|
|
1304
|
+
// const oldService = await legacyUserService(id);
|
|
1305
|
+
// return oldService.data;
|
|
1306
|
+
```
|
|
1307
|
+
|
|
1308
|
+
---
|
|
1309
|
+
|
|
1310
|
+
### Rule 13 — Repository Shape
|
|
1311
|
+
|
|
1312
|
+
Repositories must always be exported as an **object with named method keys**, not as individual exported functions:
|
|
1313
|
+
|
|
1314
|
+
```js
|
|
1315
|
+
// ✅ Correct — object with methods (easy to mock in tests)
|
|
1316
|
+
export const userRepository = {
|
|
1317
|
+
findById: async (id) => { ... },
|
|
1318
|
+
findByEmail: async (email) => { ... },
|
|
1319
|
+
create: async (data) => { ... },
|
|
1320
|
+
update: async (id, data) => { ... },
|
|
1321
|
+
remove: async (id) => { ... },
|
|
1322
|
+
};
|
|
1323
|
+
|
|
1324
|
+
// ❌ Wrong — individual exports make mocking harder
|
|
1325
|
+
export const findById = async (id) => { ... };
|
|
1326
|
+
export const findByEmail = async (email) => { ... };
|
|
1327
|
+
```
|
|
1328
|
+
|
|
1329
|
+
---
|
|
1330
|
+
|
|
1331
|
+
### Rule 14 — Never Pass `req` or `res` to a Service
|
|
1332
|
+
|
|
1333
|
+
Services are pure logic functions. They must never know they are inside an HTTP context.
|
|
1334
|
+
|
|
1335
|
+
```js
|
|
1336
|
+
// ✅ Controller extracts, service receives plain values
|
|
1337
|
+
export const getUser = asyncHandler(async (req, res) => {
|
|
1338
|
+
const { id } = req.params; // ← controller extracts
|
|
1339
|
+
const user = await getUserByIdService(id); // ← service gets plain value
|
|
1340
|
+
return ResponseUtil.success(res, "OK", user);
|
|
1341
|
+
});
|
|
1342
|
+
|
|
1343
|
+
// ❌ Never pass req or res into a service
|
|
1344
|
+
const user = await getUserByIdService(req); // ← service now depends on HTTP layer
|
|
1345
|
+
```
|
|
1346
|
+
|
|
1347
|
+
This makes services reusable and testable outside of Express (e.g. from a CLI script, a cron job, or a test).
|
|
1348
|
+
|
|
1349
|
+
---
|
|
1350
|
+
|
|
1351
|
+
### Quick Reference Card
|
|
1352
|
+
|
|
1353
|
+
```
|
|
1354
|
+
Layer Receives Returns Can call
|
|
1355
|
+
──────────────────────────────────────────────────────────────────
|
|
1356
|
+
Middleware req, res, next next() or response ResponseUtil
|
|
1357
|
+
Validation req, res, next next() or 422 ResponseUtil
|
|
1358
|
+
Controller req, res response Service, ResponseUtil
|
|
1359
|
+
Service plain values plain data Repository, logService
|
|
1360
|
+
Repository plain values plain data apiCall, DB model
|
|
1361
|
+
Util anything anything other utils only
|
|
1362
|
+
```
|
|
1363
|
+
|
|
1364
|
+
---
|
|
1365
|
+
|
|
1366
|
+
## 13. What NOT to Do
|
|
1367
|
+
|
|
1368
|
+
| ❌ Don't | ✅ Do instead |
|
|
1369
|
+
|---------|--------------|
|
|
1370
|
+
| Read `process.env.X` directly in business code | Import from `config/app.config.js` |
|
|
1371
|
+
| Call `res.json()` or `res.status()` directly | Use `ResponseUtil` methods |
|
|
1372
|
+
| Write try/catch in every controller | Wrap with `asyncHandler` and throw |
|
|
1373
|
+
| Put business logic in a controller | Move it to a service |
|
|
1374
|
+
| Put DB queries in a service | Move them to a repository |
|
|
1375
|
+
| Call `axios` directly | Use `apiCall` from `axios.util.js` |
|
|
1376
|
+
| Log raw objects with sensitive fields | Let `logService` sanitize via `sensitiveKeys` |
|
|
1377
|
+
| Leave `LOG_TYPE=1` in production | Set `LOG_TYPE=3` in production `.env` |
|
|
1378
|
+
| Add routes directly in `app.js` | Add them in `routes/index.route.js` |
|
|
1379
|
+
| Omit `.js` in import paths | Always include `.js` (ES Module requirement) |
|
|
1380
|
+
|
|
1381
|
+
---
|
|
1382
|
+
|
|
1383
|
+
## 14. CLI Package — How It Works
|
|
1384
|
+
|
|
1385
|
+
### What the CLI does
|
|
1386
|
+
|
|
1387
|
+
When you run `npx create-node-prodkit my-api`, it:
|
|
1388
|
+
|
|
1389
|
+
1. Validates the project name (letters, numbers, hyphens, underscores only).
|
|
1390
|
+
2. Copies everything inside `templates/` to a new folder named `my-api` in your current directory.
|
|
1391
|
+
3. Replaces the `{{PROJECT_NAME}}` placeholder inside `templates/package.json` with `my-api`.
|
|
1392
|
+
4. Renames `gitignore` → `.gitignore` (npm strips dotfiles from published tarballs).
|
|
1393
|
+
5. Runs `npm install` inside the new project folder.
|
|
1394
|
+
6. Prints the next-steps guide.
|
|
1395
|
+
|
|
1396
|
+
### CLI usage
|
|
1397
|
+
|
|
1398
|
+
```bash
|
|
1399
|
+
# Using npx (recommended — always gets the latest version)
|
|
1400
|
+
npx create-node-prodkit my-api
|
|
1401
|
+
|
|
1402
|
+
# Using npm init shorthand
|
|
1403
|
+
npm create node-prodkit my-api
|
|
1404
|
+
|
|
1405
|
+
# Globally installed
|
|
1406
|
+
npm install -g create-node-prodkit
|
|
1407
|
+
create-node-prodkit my-api
|
|
1408
|
+
```
|
|
1409
|
+
|
|
1410
|
+
### What gets published to npm
|
|
1411
|
+
|
|
1412
|
+
Only these three items are included in the npm tarball (controlled by `"files"` in `package.json`):
|
|
1413
|
+
|
|
1414
|
+
```
|
|
1415
|
+
bin/ ← CLI entry point
|
|
1416
|
+
templates/ ← the full skeleton that gets copied to new projects
|
|
1417
|
+
README.md ← documentation
|
|
1418
|
+
```
|
|
1419
|
+
|
|
1420
|
+
The `src/` folder, `logs/`, `.env`, and dev config files are excluded via `.npmignore`.
|
|
1421
|
+
|
|
1422
|
+
### Project layout after scaffolding
|
|
1423
|
+
|
|
1424
|
+
```
|
|
1425
|
+
my-api/
|
|
1426
|
+
├── src/ ← full skeleton ready to use
|
|
1427
|
+
├── .env.example ← copy to .env and fill in values
|
|
1428
|
+
├── .gitignore
|
|
1429
|
+
└── package.json ← name set to "my-api", deps installed
|
|
1430
|
+
```
|
|
1431
|
+
|
|
1432
|
+
---
|
|
1433
|
+
|
|
1434
|
+
## 15. Maintaining & Publishing Updates
|
|
1435
|
+
|
|
1436
|
+
### The golden rule
|
|
1437
|
+
|
|
1438
|
+
> **Whenever you change a file in `src/`, copy the same change to the matching file in `templates/src/`.**
|
|
1439
|
+
|
|
1440
|
+
The `src/` folder is the live working skeleton (for developing and testing the skeleton itself). `templates/src/` is the snapshot that gets distributed via npm.
|
|
1441
|
+
|
|
1442
|
+
---
|
|
1443
|
+
|
|
1444
|
+
### Step-by-step: making a change
|
|
1445
|
+
|
|
1446
|
+
#### 1. Make the change in `src/`
|
|
1447
|
+
|
|
1448
|
+
Edit the file normally. Test it by running `npm run dev` and verifying the behaviour.
|
|
1449
|
+
|
|
1450
|
+
#### 2. Mirror the change to `templates/src/`
|
|
1451
|
+
|
|
1452
|
+
For a single file:
|
|
1453
|
+
```bash
|
|
1454
|
+
cp src/utils/response.util.js templates/src/utils/response.util.js
|
|
1455
|
+
```
|
|
1456
|
+
|
|
1457
|
+
For a full sync of everything at once:
|
|
1458
|
+
```bash
|
|
1459
|
+
node --input-type=module --eval "
|
|
1460
|
+
import { cpSync } from 'fs';
|
|
1461
|
+
cpSync('src', 'templates/src', { recursive: true });
|
|
1462
|
+
console.log('templates/src synced');
|
|
1463
|
+
"
|
|
1464
|
+
```
|
|
1465
|
+
|
|
1466
|
+
#### 3. Bump the version — choose the right level
|
|
1467
|
+
|
|
1468
|
+
| Change type | Version bump | Command |
|
|
1469
|
+
|-------------|-------------|---------|
|
|
1470
|
+
| Bug fix, minor tweak | patch `1.0.0 → 1.0.1` | `npm version patch` |
|
|
1471
|
+
| New feature, new utility | minor `1.0.0 → 1.1.0` | `npm version minor` |
|
|
1472
|
+
| Breaking change (renames, removals) | major `1.0.0 → 2.0.0` | `npm version major` |
|
|
1473
|
+
|
|
1474
|
+
`npm version` automatically:
|
|
1475
|
+
- Updates `version` in `package.json`
|
|
1476
|
+
- Creates a git commit
|
|
1477
|
+
- Creates a git tag
|
|
1478
|
+
|
|
1479
|
+
#### 4. Log the change in CHANGELOG.md
|
|
1480
|
+
|
|
1481
|
+
```md
|
|
1482
|
+
## [1.1.0] - 2026-02-21
|
|
1483
|
+
### Added
|
|
1484
|
+
- `logService` now supports an `"external"` mode posting to a remote URL
|
|
1485
|
+
|
|
1486
|
+
### Changed
|
|
1487
|
+
- `asyncHandler` improved error propagation
|
|
1488
|
+
|
|
1489
|
+
### Fixed
|
|
1490
|
+
- nodemon config path corrected to `src/server.js`
|
|
1491
|
+
```
|
|
1492
|
+
|
|
1493
|
+
#### 5. Publish to npm
|
|
1494
|
+
|
|
1495
|
+
**First-time setup:**
|
|
1496
|
+
```bash
|
|
1497
|
+
npm login # log in to your npm account
|
|
1498
|
+
```
|
|
1499
|
+
|
|
1500
|
+
**Every release:**
|
|
1501
|
+
```bash
|
|
1502
|
+
# Preview exactly what will be published before pushing
|
|
1503
|
+
npm pack --dry-run
|
|
1504
|
+
|
|
1505
|
+
# Publish
|
|
1506
|
+
npm publish
|
|
1507
|
+
|
|
1508
|
+
# Or publish with a tag (e.g., for a beta)
|
|
1509
|
+
npm publish --tag beta
|
|
1510
|
+
```
|
|
1511
|
+
|
|
1512
|
+
---
|
|
1513
|
+
|
|
1514
|
+
### Quick release checklist
|
|
1515
|
+
|
|
1516
|
+
```
|
|
1517
|
+
[ ] Change made in src/
|
|
1518
|
+
[ ] Same change mirrored to templates/src/
|
|
1519
|
+
[ ] CHANGELOG.md updated
|
|
1520
|
+
[ ] npm version patch|minor|major (updates package.json + git tag)
|
|
1521
|
+
[ ] git push && git push --tags
|
|
1522
|
+
[ ] npm publish
|
|
1523
|
+
[ ] Verify: npx create-node-prodkit@latest test-project
|
|
1524
|
+
```
|
|
1525
|
+
|
|
1526
|
+
---
|
|
1527
|
+
|
|
1528
|
+
### What each file in this repo does for the CLI
|
|
1529
|
+
|
|
1530
|
+
| File / Folder | Purpose |
|
|
1531
|
+
|--------------|---------|
|
|
1532
|
+
| `bin/create.js` | CLI executable — parses args, copies templates, runs npm install |
|
|
1533
|
+
| `templates/` | Snapshot of the skeleton distributed via npm |
|
|
1534
|
+
| `templates/package.json` | Has `{{PROJECT_NAME}}` placeholder replaced at scaffold time |
|
|
1535
|
+
| `templates/gitignore` | Renamed to `.gitignore` during scaffold (npm strips dotfiles) |
|
|
1536
|
+
| `templates/.env.example` | All env vars documented with safe defaults |
|
|
1537
|
+
| `src/` | Live working skeleton for development — **not published to npm** |
|
|
1538
|
+
| `.npmignore` | Excludes `src/`, logs, `.env`, editor config from the tarball |
|
|
1539
|
+
| `package.json` `"files"` | Allowlist: only `bin/`, `templates/`, `README.md` go to npm |
|
|
1540
|
+
|