create-listablelabs-api 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +142 -0
- package/bin/cli.js +37 -0
- package/bin/commands/add.js +460 -0
- package/bin/commands/create.js +481 -0
- package/package.json +39 -0
- package/templates/base/.dockerignore +19 -0
- package/templates/base/.env.example +18 -0
- package/templates/base/.eslintrc.js +31 -0
- package/templates/base/Dockerfile +48 -0
- package/templates/base/README.md +295 -0
- package/templates/base/docker-compose.yml +55 -0
- package/templates/base/jest.config.js +24 -0
- package/templates/base/package.json +41 -0
- package/templates/base/src/app.js +103 -0
- package/templates/base/src/config/index.js +36 -0
- package/templates/base/src/controllers/exampleController.js +148 -0
- package/templates/base/src/database/baseModel.js +160 -0
- package/templates/base/src/database/index.js +108 -0
- package/templates/base/src/middlewares/errorHandler.js +155 -0
- package/templates/base/src/middlewares/index.js +49 -0
- package/templates/base/src/middlewares/rateLimiter.js +85 -0
- package/templates/base/src/middlewares/requestLogger.js +50 -0
- package/templates/base/src/middlewares/validator.js +107 -0
- package/templates/base/src/models/example.js +117 -0
- package/templates/base/src/models/index.js +6 -0
- package/templates/base/src/routes/v1/exampleRoutes.js +89 -0
- package/templates/base/src/routes/v1/index.js +19 -0
- package/templates/base/src/server.js +80 -0
- package/templates/base/src/utils/logger.js +61 -0
- package/templates/base/src/utils/response.js +117 -0
- package/templates/base/tests/app.test.js +215 -0
- package/templates/base/tests/setup.js +33 -0
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
# ListableLabs Microservice Template
|
|
2
|
+
|
|
3
|
+
A production-ready Node.js/Express microservice template with MongoDB Atlas, Zod validation, Pino logging, and best practices.
|
|
4
|
+
|
|
5
|
+
## 🚀 Quick Start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# Clone and setup
|
|
9
|
+
git clone <repo-url> my-service
|
|
10
|
+
cd my-service
|
|
11
|
+
npm install
|
|
12
|
+
|
|
13
|
+
# Configure environment
|
|
14
|
+
cp .env.example .env
|
|
15
|
+
# Edit .env with your MongoDB Atlas URI and service name
|
|
16
|
+
|
|
17
|
+
# Start development
|
|
18
|
+
npm run dev
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## 📁 Project Structure
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
├── src/
|
|
25
|
+
│ ├── config/ # Configuration (env vars)
|
|
26
|
+
│ ├── controllers/ # Request handlers
|
|
27
|
+
│ ├── database/ # MongoDB connection & base model
|
|
28
|
+
│ ├── middlewares/ # Express middlewares
|
|
29
|
+
│ │ ├── errorHandler.js # Centralized error handling
|
|
30
|
+
│ │ ├── rateLimiter.js # Rate limiting
|
|
31
|
+
│ │ ├── requestLogger.js # Pino HTTP logging
|
|
32
|
+
│ │ └── validator.js # Zod validation
|
|
33
|
+
│ ├── models/ # Mongoose models + Zod schemas
|
|
34
|
+
│ ├── routes/v1/ # API routes
|
|
35
|
+
│ ├── utils/
|
|
36
|
+
│ │ ├── logger.js # Pino logger
|
|
37
|
+
│ │ └── response.js # Standard responses
|
|
38
|
+
│ ├── app.js # Express setup
|
|
39
|
+
│ └── server.js # Entry point
|
|
40
|
+
├── tests/ # Jest tests with mongodb-memory-server
|
|
41
|
+
├── Dockerfile
|
|
42
|
+
└── docker-compose.yml
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## 🔧 Scripts
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npm start # Production server
|
|
49
|
+
npm run dev # Development with nodemon
|
|
50
|
+
npm test # Run tests with coverage
|
|
51
|
+
npm run lint # ESLint check
|
|
52
|
+
npm run lint:fix # Auto-fix lint errors
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## 📝 Creating New Endpoints
|
|
56
|
+
|
|
57
|
+
### 1. Create Model with Zod Schema
|
|
58
|
+
|
|
59
|
+
```javascript
|
|
60
|
+
// src/models/user.js
|
|
61
|
+
const { createModel } = require('../database/baseModel');
|
|
62
|
+
const { z } = require('zod');
|
|
63
|
+
|
|
64
|
+
// Zod schemas for validation
|
|
65
|
+
const userZodSchema = {
|
|
66
|
+
create: z.object({
|
|
67
|
+
email: z.string().email(),
|
|
68
|
+
name: z.string().min(1).max(100),
|
|
69
|
+
role: z.enum(['user', 'admin']).default('user'),
|
|
70
|
+
}),
|
|
71
|
+
update: z.object({
|
|
72
|
+
name: z.string().min(1).max(100).optional(),
|
|
73
|
+
role: z.enum(['user', 'admin']).optional(),
|
|
74
|
+
}),
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// Mongoose model
|
|
78
|
+
const User = createModel('User', {
|
|
79
|
+
email: { type: String, required: true, unique: true },
|
|
80
|
+
name: { type: String, required: true },
|
|
81
|
+
role: { type: String, enum: ['user', 'admin'], default: 'user' },
|
|
82
|
+
}, {
|
|
83
|
+
softDelete: true, // enables soft delete
|
|
84
|
+
indexes: [{ fields: { email: 1 }, options: { unique: true } }],
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
module.exports = { User, userZodSchema };
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### 2. Create Controller
|
|
91
|
+
|
|
92
|
+
```javascript
|
|
93
|
+
// src/controllers/userController.js
|
|
94
|
+
const { asyncHandler, NotFoundError } = require('../middlewares');
|
|
95
|
+
const { sendSuccess, sendCreated, sendPaginated } = require('../utils/response');
|
|
96
|
+
const { User } = require('../models');
|
|
97
|
+
|
|
98
|
+
const getAll = asyncHandler(async (req, res) => {
|
|
99
|
+
const result = await User.paginate({}, req.query);
|
|
100
|
+
sendPaginated(res, result.data, result.pagination);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const create = asyncHandler(async (req, res) => {
|
|
104
|
+
const user = await User.create(req.body);
|
|
105
|
+
sendCreated(res, user);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
module.exports = { getAll, create };
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### 3. Create Routes with Validation
|
|
112
|
+
|
|
113
|
+
```javascript
|
|
114
|
+
// src/routes/v1/userRoutes.js
|
|
115
|
+
const { Router } = require('express');
|
|
116
|
+
const { validate, z } = require('../../middlewares');
|
|
117
|
+
const { userZodSchema } = require('../../models');
|
|
118
|
+
const userController = require('../../controllers/userController');
|
|
119
|
+
|
|
120
|
+
const router = Router();
|
|
121
|
+
|
|
122
|
+
router.get('/', userController.getAll);
|
|
123
|
+
router.post('/', validate({ body: userZodSchema.create }), userController.create);
|
|
124
|
+
|
|
125
|
+
module.exports = router;
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### 4. Register in routes/v1/index.js
|
|
129
|
+
|
|
130
|
+
```javascript
|
|
131
|
+
const userRoutes = require('./userRoutes');
|
|
132
|
+
router.use('/users', userRoutes);
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## ✅ Validation with Zod
|
|
136
|
+
|
|
137
|
+
```javascript
|
|
138
|
+
const { validate, z, schemas } = require('./middlewares');
|
|
139
|
+
|
|
140
|
+
// Custom schema
|
|
141
|
+
const schema = {
|
|
142
|
+
body: z.object({
|
|
143
|
+
email: z.string().email(),
|
|
144
|
+
age: z.number().int().min(18),
|
|
145
|
+
}),
|
|
146
|
+
query: z.object({
|
|
147
|
+
page: z.coerce.number().int().min(1).default(1),
|
|
148
|
+
}),
|
|
149
|
+
params: z.object({
|
|
150
|
+
id: schemas.mongoId, // reusable pattern
|
|
151
|
+
}),
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
router.post('/users/:id', validate(schema), controller.update);
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Reusable Schema Patterns
|
|
158
|
+
|
|
159
|
+
```javascript
|
|
160
|
+
const { schemas } = require('./middlewares');
|
|
161
|
+
|
|
162
|
+
schemas.mongoId // MongoDB ObjectId validation
|
|
163
|
+
schemas.email // Email with lowercase trim
|
|
164
|
+
schemas.pagination // page, limit, sortBy, sortOrder
|
|
165
|
+
schemas.dateRange // startDate, endDate validation
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## 🪵 Logging with Pino
|
|
169
|
+
|
|
170
|
+
```javascript
|
|
171
|
+
const { logger, createChildLogger } = require('./utils/logger');
|
|
172
|
+
|
|
173
|
+
// Direct logging
|
|
174
|
+
logger.info({ userId: 123 }, 'User logged in');
|
|
175
|
+
logger.error({ err }, 'Database error');
|
|
176
|
+
|
|
177
|
+
// Child logger with context
|
|
178
|
+
const log = createChildLogger({ module: 'paymentService' });
|
|
179
|
+
log.info({ orderId: 456 }, 'Processing payment');
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
**Log Output (JSON in production):**
|
|
183
|
+
```json
|
|
184
|
+
{
|
|
185
|
+
"level": 30,
|
|
186
|
+
"time": "2024-01-15T10:30:00.000Z",
|
|
187
|
+
"service": "my-service",
|
|
188
|
+
"module": "paymentService",
|
|
189
|
+
"orderId": 456,
|
|
190
|
+
"msg": "Processing payment"
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## ❌ Error Handling
|
|
195
|
+
|
|
196
|
+
```javascript
|
|
197
|
+
const {
|
|
198
|
+
BadRequestError, // 400
|
|
199
|
+
UnauthorizedError, // 401
|
|
200
|
+
ForbiddenError, // 403
|
|
201
|
+
NotFoundError, // 404
|
|
202
|
+
ConflictError, // 409
|
|
203
|
+
ValidationError, // 422
|
|
204
|
+
TooManyRequestsError, // 429
|
|
205
|
+
} = require('./middlewares');
|
|
206
|
+
|
|
207
|
+
// Throw anywhere - caught by error handler
|
|
208
|
+
throw new NotFoundError('User not found');
|
|
209
|
+
throw new ValidationError('Invalid input', [
|
|
210
|
+
{ field: 'email', message: 'Invalid format' }
|
|
211
|
+
]);
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
**Error Response:**
|
|
215
|
+
```json
|
|
216
|
+
{
|
|
217
|
+
"success": false,
|
|
218
|
+
"error": {
|
|
219
|
+
"code": "NOT_FOUND",
|
|
220
|
+
"message": "User not found"
|
|
221
|
+
},
|
|
222
|
+
"requestId": "abc-123",
|
|
223
|
+
"timestamp": "2024-01-15T10:30:00.000Z"
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## 📊 Standard Responses
|
|
228
|
+
|
|
229
|
+
```javascript
|
|
230
|
+
const { sendSuccess, sendCreated, sendPaginated, sendNoContent } = require('./utils/response');
|
|
231
|
+
|
|
232
|
+
sendSuccess(res, data); // 200
|
|
233
|
+
sendCreated(res, newUser); // 201
|
|
234
|
+
sendPaginated(res, items, pagination); // 200 with pagination
|
|
235
|
+
sendNoContent(res); // 204
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
## 🗄️ MongoDB Atlas Setup
|
|
239
|
+
|
|
240
|
+
1. Create cluster at [MongoDB Atlas](https://www.mongodb.com/atlas)
|
|
241
|
+
2. Get connection string: `mongodb+srv://<user>:<pass>@cluster.mongodb.net/<db>`
|
|
242
|
+
3. Add to `.env`:
|
|
243
|
+
```
|
|
244
|
+
MONGODB_URI=mongodb+srv://username:password@cluster.mongodb.net/mydb?retryWrites=true&w=majority
|
|
245
|
+
```
|
|
246
|
+
4. Whitelist your IP in Atlas Network Access
|
|
247
|
+
|
|
248
|
+
## 🧪 Testing
|
|
249
|
+
|
|
250
|
+
Tests use `mongodb-memory-server` for isolated in-memory MongoDB:
|
|
251
|
+
|
|
252
|
+
```bash
|
|
253
|
+
npm test # Run all tests
|
|
254
|
+
npm run test:watch # Watch mode
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
```javascript
|
|
258
|
+
// tests/user.test.js
|
|
259
|
+
const request = require('supertest');
|
|
260
|
+
const app = require('../src/app');
|
|
261
|
+
const { User } = require('../src/models');
|
|
262
|
+
|
|
263
|
+
describe('POST /api/v1/users', () => {
|
|
264
|
+
it('should create user', async () => {
|
|
265
|
+
const res = await request(app)
|
|
266
|
+
.post('/api/v1/users')
|
|
267
|
+
.send({ email: 'test@example.com', name: 'Test' });
|
|
268
|
+
|
|
269
|
+
expect(res.status).toBe(201);
|
|
270
|
+
expect(res.body.data.email).toBe('test@example.com');
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
## 🐳 Docker
|
|
276
|
+
|
|
277
|
+
```bash
|
|
278
|
+
docker build -t my-service .
|
|
279
|
+
docker run -p 3000:3000 -e MONGODB_URI=... my-service
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
## 🏥 Health Checks
|
|
283
|
+
|
|
284
|
+
- `GET /health` - Liveness (always 200 if running)
|
|
285
|
+
- `GET /ready` - Readiness (checks DB connection)
|
|
286
|
+
|
|
287
|
+
## 📋 New Service Checklist
|
|
288
|
+
|
|
289
|
+
- [ ] Update `SERVICE_NAME` in `.env`
|
|
290
|
+
- [ ] Update `name` in `package.json`
|
|
291
|
+
- [ ] Configure `MONGODB_URI` for your database
|
|
292
|
+
- [ ] Remove example routes/models
|
|
293
|
+
- [ ] Add your models in `src/models/`
|
|
294
|
+
- [ ] Add your routes in `src/routes/v1/`
|
|
295
|
+
- [ ] Write tests
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
version: '3.8'
|
|
2
|
+
|
|
3
|
+
services:
|
|
4
|
+
api:
|
|
5
|
+
build:
|
|
6
|
+
context: .
|
|
7
|
+
target: production
|
|
8
|
+
ports:
|
|
9
|
+
- "3000:3000"
|
|
10
|
+
environment:
|
|
11
|
+
- NODE_ENV=production
|
|
12
|
+
- PORT=3000
|
|
13
|
+
- SERVICE_NAME=my-service
|
|
14
|
+
- LOG_LEVEL=info
|
|
15
|
+
healthcheck:
|
|
16
|
+
test: ["CMD", "node", "-e", "require('http').get('http://localhost:3000/health', (r) => process.exit(r.statusCode === 200 ? 0 : 1))"]
|
|
17
|
+
interval: 30s
|
|
18
|
+
timeout: 3s
|
|
19
|
+
retries: 3
|
|
20
|
+
start_period: 5s
|
|
21
|
+
restart: unless-stopped
|
|
22
|
+
|
|
23
|
+
# Uncomment and configure as needed:
|
|
24
|
+
|
|
25
|
+
# redis:
|
|
26
|
+
# image: redis:7-alpine
|
|
27
|
+
# ports:
|
|
28
|
+
# - "6379:6379"
|
|
29
|
+
# volumes:
|
|
30
|
+
# - redis-data:/data
|
|
31
|
+
# healthcheck:
|
|
32
|
+
# test: ["CMD", "redis-cli", "ping"]
|
|
33
|
+
# interval: 10s
|
|
34
|
+
# timeout: 5s
|
|
35
|
+
# retries: 5
|
|
36
|
+
|
|
37
|
+
# postgres:
|
|
38
|
+
# image: postgres:15-alpine
|
|
39
|
+
# ports:
|
|
40
|
+
# - "5432:5432"
|
|
41
|
+
# environment:
|
|
42
|
+
# - POSTGRES_USER=user
|
|
43
|
+
# - POSTGRES_PASSWORD=password
|
|
44
|
+
# - POSTGRES_DB=mydb
|
|
45
|
+
# volumes:
|
|
46
|
+
# - postgres-data:/var/lib/postgresql/data
|
|
47
|
+
# healthcheck:
|
|
48
|
+
# test: ["CMD-SHELL", "pg_isready -U user -d mydb"]
|
|
49
|
+
# interval: 10s
|
|
50
|
+
# timeout: 5s
|
|
51
|
+
# retries: 5
|
|
52
|
+
|
|
53
|
+
# volumes:
|
|
54
|
+
# redis-data:
|
|
55
|
+
# postgres-data:
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
testEnvironment: 'node',
|
|
3
|
+
coverageDirectory: 'coverage',
|
|
4
|
+
collectCoverageFrom: [
|
|
5
|
+
'src/**/*.js',
|
|
6
|
+
'!src/server.js',
|
|
7
|
+
],
|
|
8
|
+
coverageThreshold: {
|
|
9
|
+
global: {
|
|
10
|
+
branches: 70,
|
|
11
|
+
functions: 70,
|
|
12
|
+
lines: 70,
|
|
13
|
+
statements: 70,
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
testMatch: ['**/tests/**/*.test.js'],
|
|
17
|
+
setupFilesAfterEnv: ['./tests/setup.js'],
|
|
18
|
+
verbose: true,
|
|
19
|
+
forceExit: true,
|
|
20
|
+
clearMocks: true,
|
|
21
|
+
resetMocks: true,
|
|
22
|
+
restoreMocks: true,
|
|
23
|
+
testTimeout: 30000,
|
|
24
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "listablelabs-service",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Microservice template - replace service-name with your service",
|
|
5
|
+
"main": "src/server.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"start": "node src/server.js",
|
|
8
|
+
"dev": "nodemon src/server.js",
|
|
9
|
+
"test": "jest --coverage",
|
|
10
|
+
"test:watch": "jest --watch",
|
|
11
|
+
"lint": "eslint src/",
|
|
12
|
+
"lint:fix": "eslint src/ --fix",
|
|
13
|
+
"docker:build": "docker build -t service-name .",
|
|
14
|
+
"docker:run": "docker run -p 3000:3000 service-name"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"express": "^4.18.2",
|
|
18
|
+
"helmet": "^7.1.0",
|
|
19
|
+
"cors": "^2.8.5",
|
|
20
|
+
"compression": "^1.7.4",
|
|
21
|
+
"pino": "^8.17.2",
|
|
22
|
+
"pino-http": "^9.0.0",
|
|
23
|
+
"zod": "^3.22.4",
|
|
24
|
+
"dotenv": "^16.3.1",
|
|
25
|
+
"express-rate-limit": "^7.1.5",
|
|
26
|
+
"uuid": "^9.0.1",
|
|
27
|
+
"http-status-codes": "^2.3.0",
|
|
28
|
+
"mongoose": "^8.0.3"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"nodemon": "^3.0.2",
|
|
32
|
+
"jest": "^29.7.0",
|
|
33
|
+
"supertest": "^6.3.3",
|
|
34
|
+
"eslint": "^8.56.0",
|
|
35
|
+
"pino-pretty": "^10.3.1",
|
|
36
|
+
"mongodb-memory-server": "^9.1.6"
|
|
37
|
+
},
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=18.0.0"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const helmet = require('helmet');
|
|
3
|
+
const cors = require('cors');
|
|
4
|
+
const compression = require('compression');
|
|
5
|
+
const config = require('./config');
|
|
6
|
+
const {
|
|
7
|
+
requestLogger,
|
|
8
|
+
errorHandler,
|
|
9
|
+
notFoundHandler,
|
|
10
|
+
defaultLimiter,
|
|
11
|
+
} = require('./middlewares');
|
|
12
|
+
const { healthCheck, getConnectionStatus } = require('./database');
|
|
13
|
+
const routes = require('./routes/v1');
|
|
14
|
+
|
|
15
|
+
const app = express();
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Security middleware
|
|
19
|
+
*/
|
|
20
|
+
app.use(helmet()); // Security headers
|
|
21
|
+
app.use(cors()); // CORS handling
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Request parsing
|
|
25
|
+
*/
|
|
26
|
+
app.use(express.json({ limit: '10kb' })); // Parse JSON bodies
|
|
27
|
+
app.use(express.urlencoded({ extended: true, limit: '10kb' })); // Parse URL-encoded bodies
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Compression
|
|
31
|
+
*/
|
|
32
|
+
app.use(compression());
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Request logging - logs all incoming requests
|
|
36
|
+
*/
|
|
37
|
+
app.use(requestLogger);
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Rate limiting - applies to all routes
|
|
41
|
+
*/
|
|
42
|
+
app.use(defaultLimiter);
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Health check endpoint (liveness probe)
|
|
46
|
+
* Returns 200 if the service is running
|
|
47
|
+
*/
|
|
48
|
+
app.get('/health', (req, res) => {
|
|
49
|
+
res.json({
|
|
50
|
+
status: 'healthy',
|
|
51
|
+
service: config.serviceName,
|
|
52
|
+
timestamp: new Date().toISOString(),
|
|
53
|
+
uptime: process.uptime(),
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Readiness check endpoint (readiness probe)
|
|
59
|
+
* Returns 200 only if all dependencies are ready
|
|
60
|
+
*/
|
|
61
|
+
app.get('/ready', async (req, res) => {
|
|
62
|
+
const dbHealthy = await healthCheck();
|
|
63
|
+
|
|
64
|
+
const status = {
|
|
65
|
+
service: config.serviceName,
|
|
66
|
+
timestamp: new Date().toISOString(),
|
|
67
|
+
checks: {
|
|
68
|
+
database: {
|
|
69
|
+
status: dbHealthy ? 'healthy' : 'unhealthy',
|
|
70
|
+
connection: getConnectionStatus(),
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
if (!dbHealthy) {
|
|
76
|
+
return res.status(503).json({
|
|
77
|
+
status: 'not ready',
|
|
78
|
+
...status,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
res.json({
|
|
83
|
+
status: 'ready',
|
|
84
|
+
...status,
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* API routes
|
|
90
|
+
*/
|
|
91
|
+
app.use('/api/v1', routes);
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* 404 handler for undefined routes
|
|
95
|
+
*/
|
|
96
|
+
app.use(notFoundHandler);
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Global error handler - must be last
|
|
100
|
+
*/
|
|
101
|
+
app.use(errorHandler);
|
|
102
|
+
|
|
103
|
+
module.exports = app;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
require('dotenv').config();
|
|
2
|
+
|
|
3
|
+
const config = {
|
|
4
|
+
// Application
|
|
5
|
+
env: process.env.NODE_ENV || 'development',
|
|
6
|
+
port: parseInt(process.env.PORT, 10) || 3000,
|
|
7
|
+
serviceName: process.env.SERVICE_NAME || 'unknown-service',
|
|
8
|
+
|
|
9
|
+
// Logging
|
|
10
|
+
logLevel: process.env.LOG_LEVEL || 'info',
|
|
11
|
+
|
|
12
|
+
// Rate Limiting
|
|
13
|
+
rateLimit: {
|
|
14
|
+
windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS, 10) || 60000,
|
|
15
|
+
maxRequests: parseInt(process.env.RATE_LIMIT_MAX_REQUESTS, 10) || 100,
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
// MongoDB Atlas
|
|
19
|
+
mongoUri: process.env.MONGODB_URI,
|
|
20
|
+
|
|
21
|
+
// JWT (configure as needed)
|
|
22
|
+
// jwt: {
|
|
23
|
+
// secret: process.env.JWT_SECRET,
|
|
24
|
+
// expiresIn: process.env.JWT_EXPIRES_IN || '1d',
|
|
25
|
+
// },
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// Validate required config
|
|
29
|
+
const requiredEnvVars = ['SERVICE_NAME', 'MONGODB_URI'];
|
|
30
|
+
const missingEnvVars = requiredEnvVars.filter((envVar) => !process.env[envVar]);
|
|
31
|
+
|
|
32
|
+
if (missingEnvVars.length > 0 && process.env.NODE_ENV !== 'test') {
|
|
33
|
+
throw new Error(`Missing required environment variables: ${missingEnvVars.join(', ')}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports = config;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
const { asyncHandler, NotFoundError } = require('../middlewares');
|
|
2
|
+
const { sendSuccess, sendCreated, sendPaginated, sendNoContent } = require('../utils/response');
|
|
3
|
+
const { createChildLogger } = require('../utils/logger');
|
|
4
|
+
const { Example } = require('../models');
|
|
5
|
+
|
|
6
|
+
const log = createChildLogger({ module: 'exampleController' });
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Get all examples with pagination
|
|
10
|
+
* GET /api/v1/examples
|
|
11
|
+
*/
|
|
12
|
+
const getAll = asyncHandler(async (req, res) => {
|
|
13
|
+
const { page, limit, status, search, sortBy, sortOrder } = req.query;
|
|
14
|
+
|
|
15
|
+
log.info({ page, limit, status, search }, 'Fetching examples');
|
|
16
|
+
|
|
17
|
+
const result = await Example.search(
|
|
18
|
+
{ status, search },
|
|
19
|
+
{ page, limit, sortBy, sortOrder }
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
sendPaginated(res, result.data, result.pagination);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Get example by ID
|
|
27
|
+
* GET /api/v1/examples/:id
|
|
28
|
+
*/
|
|
29
|
+
const getById = asyncHandler(async (req, res) => {
|
|
30
|
+
const { id } = req.params;
|
|
31
|
+
|
|
32
|
+
log.info({ id }, 'Fetching example by ID');
|
|
33
|
+
|
|
34
|
+
const example = await Example.findById(id);
|
|
35
|
+
|
|
36
|
+
if (!example) {
|
|
37
|
+
throw new NotFoundError(`Example with ID ${id} not found`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
sendSuccess(res, example);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Create new example
|
|
45
|
+
* POST /api/v1/examples
|
|
46
|
+
*/
|
|
47
|
+
const create = asyncHandler(async (req, res) => {
|
|
48
|
+
const data = req.body;
|
|
49
|
+
|
|
50
|
+
log.info({ data }, 'Creating new example');
|
|
51
|
+
|
|
52
|
+
const example = await Example.create(data);
|
|
53
|
+
|
|
54
|
+
sendCreated(res, example);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Update example
|
|
59
|
+
* PUT /api/v1/examples/:id
|
|
60
|
+
*/
|
|
61
|
+
const update = asyncHandler(async (req, res) => {
|
|
62
|
+
const { id } = req.params;
|
|
63
|
+
const data = req.body;
|
|
64
|
+
|
|
65
|
+
log.info({ id, data }, 'Updating example');
|
|
66
|
+
|
|
67
|
+
const example = await Example.findByIdAndUpdate(
|
|
68
|
+
id,
|
|
69
|
+
data,
|
|
70
|
+
{ new: true, runValidators: true }
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
if (!example) {
|
|
74
|
+
throw new NotFoundError(`Example with ID ${id} not found`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
sendSuccess(res, example, 200, 'Example updated successfully');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Delete example (soft delete)
|
|
82
|
+
* DELETE /api/v1/examples/:id
|
|
83
|
+
*/
|
|
84
|
+
const remove = asyncHandler(async (req, res) => {
|
|
85
|
+
const { id } = req.params;
|
|
86
|
+
|
|
87
|
+
log.info({ id }, 'Deleting example');
|
|
88
|
+
|
|
89
|
+
const example = await Example.findById(id);
|
|
90
|
+
|
|
91
|
+
if (!example) {
|
|
92
|
+
throw new NotFoundError(`Example with ID ${id} not found`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
await example.softDelete();
|
|
96
|
+
|
|
97
|
+
sendNoContent(res);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Activate example
|
|
102
|
+
* PATCH /api/v1/examples/:id/activate
|
|
103
|
+
*/
|
|
104
|
+
const activate = asyncHandler(async (req, res) => {
|
|
105
|
+
const { id } = req.params;
|
|
106
|
+
|
|
107
|
+
log.info({ id }, 'Activating example');
|
|
108
|
+
|
|
109
|
+
const example = await Example.findById(id);
|
|
110
|
+
|
|
111
|
+
if (!example) {
|
|
112
|
+
throw new NotFoundError(`Example with ID ${id} not found`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
await example.activate();
|
|
116
|
+
|
|
117
|
+
sendSuccess(res, example, 200, 'Example activated');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Deactivate example
|
|
122
|
+
* PATCH /api/v1/examples/:id/deactivate
|
|
123
|
+
*/
|
|
124
|
+
const deactivate = asyncHandler(async (req, res) => {
|
|
125
|
+
const { id } = req.params;
|
|
126
|
+
|
|
127
|
+
log.info({ id }, 'Deactivating example');
|
|
128
|
+
|
|
129
|
+
const example = await Example.findById(id);
|
|
130
|
+
|
|
131
|
+
if (!example) {
|
|
132
|
+
throw new NotFoundError(`Example with ID ${id} not found`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
await example.deactivate();
|
|
136
|
+
|
|
137
|
+
sendSuccess(res, example, 200, 'Example deactivated');
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
module.exports = {
|
|
141
|
+
getAll,
|
|
142
|
+
getById,
|
|
143
|
+
create,
|
|
144
|
+
update,
|
|
145
|
+
remove,
|
|
146
|
+
activate,
|
|
147
|
+
deactivate,
|
|
148
|
+
};
|