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,481 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const inquirer = require('inquirer');
|
|
5
|
+
const ora = require('ora');
|
|
6
|
+
const { execSync } = require('child_process');
|
|
7
|
+
|
|
8
|
+
const TEMPLATE_DIR = path.join(__dirname, '..', '..', 'templates', 'base');
|
|
9
|
+
|
|
10
|
+
async function create(projectName, options) {
|
|
11
|
+
console.log(chalk.cyan(`
|
|
12
|
+
╦ ╦╔═╗╔╦╗╔═╗╔╗ ╦ ╔═╗╦ ╔═╗╔╗ ╔═╗
|
|
13
|
+
║ ║╚═╗ ║ ╠═╣╠╩╗║ ║╣ ║ ╠═╣╠╩╗╚═╗
|
|
14
|
+
╩═╝╩╚═╝ ╩ ╩ ╩╚═╝╩═╝╚═╝╩═╝╩ ╩╚═╝╚═╝
|
|
15
|
+
`));
|
|
16
|
+
console.log(chalk.gray(' Microservice Generator\n'));
|
|
17
|
+
|
|
18
|
+
// Validate project name
|
|
19
|
+
const validName = projectName
|
|
20
|
+
.toLowerCase()
|
|
21
|
+
.replace(/[^a-z0-9-]/g, '-')
|
|
22
|
+
.replace(/-+/g, '-')
|
|
23
|
+
.replace(/^-|-$/g, '');
|
|
24
|
+
|
|
25
|
+
if (validName !== projectName) {
|
|
26
|
+
console.log(chalk.yellow(` Project name normalized to: ${validName}\n`));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const targetDir = path.resolve(options.directory, validName);
|
|
30
|
+
|
|
31
|
+
// Check if directory exists
|
|
32
|
+
if (fs.existsSync(targetDir)) {
|
|
33
|
+
const { overwrite } = await inquirer.prompt([
|
|
34
|
+
{
|
|
35
|
+
type: 'confirm',
|
|
36
|
+
name: 'overwrite',
|
|
37
|
+
message: `Directory ${validName} already exists. Overwrite?`,
|
|
38
|
+
default: false,
|
|
39
|
+
},
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
if (!overwrite) {
|
|
43
|
+
console.log(chalk.red(' Aborted.'));
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
fs.removeSync(targetDir);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Get project configuration
|
|
50
|
+
let config;
|
|
51
|
+
if (options.yes) {
|
|
52
|
+
config = {
|
|
53
|
+
serviceName: validName,
|
|
54
|
+
description: `${validName} microservice`,
|
|
55
|
+
port: 3000,
|
|
56
|
+
features: ['logging', 'validation', 'rate-limiting', 'error-handling'],
|
|
57
|
+
mongodbUri: '',
|
|
58
|
+
};
|
|
59
|
+
} else {
|
|
60
|
+
config = await inquirer.prompt([
|
|
61
|
+
{
|
|
62
|
+
type: 'input',
|
|
63
|
+
name: 'serviceName',
|
|
64
|
+
message: 'Service name:',
|
|
65
|
+
default: validName,
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
type: 'input',
|
|
69
|
+
name: 'description',
|
|
70
|
+
message: 'Description:',
|
|
71
|
+
default: `${validName} microservice`,
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
type: 'number',
|
|
75
|
+
name: 'port',
|
|
76
|
+
message: 'Port:',
|
|
77
|
+
default: 3000,
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
type: 'input',
|
|
81
|
+
name: 'mongodbUri',
|
|
82
|
+
message: 'MongoDB Atlas URI (leave blank to configure later):',
|
|
83
|
+
default: '',
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
type: 'checkbox',
|
|
87
|
+
name: 'features',
|
|
88
|
+
message: 'Select additional features:',
|
|
89
|
+
choices: [
|
|
90
|
+
{ name: 'JWT Authentication', value: 'auth', checked: false },
|
|
91
|
+
{ name: 'Swagger/OpenAPI Docs', value: 'swagger', checked: false },
|
|
92
|
+
],
|
|
93
|
+
},
|
|
94
|
+
]);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Create project
|
|
98
|
+
const spinner = ora('Creating project structure...').start();
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
// Copy template files
|
|
102
|
+
await fs.copy(TEMPLATE_DIR, targetDir);
|
|
103
|
+
|
|
104
|
+
// Update package.json
|
|
105
|
+
const pkgPath = path.join(targetDir, 'package.json');
|
|
106
|
+
const pkg = await fs.readJson(pkgPath);
|
|
107
|
+
pkg.name = config.serviceName;
|
|
108
|
+
pkg.description = config.description;
|
|
109
|
+
|
|
110
|
+
// Add auth dependencies if selected
|
|
111
|
+
if (config.features.includes('auth')) {
|
|
112
|
+
pkg.dependencies.jsonwebtoken = '^9.0.2';
|
|
113
|
+
pkg.dependencies.bcryptjs = '^2.4.3';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Add swagger if selected
|
|
117
|
+
if (config.features.includes('swagger')) {
|
|
118
|
+
pkg.dependencies['swagger-jsdoc'] = '^6.2.8';
|
|
119
|
+
pkg.dependencies['swagger-ui-express'] = '^5.0.0';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
await fs.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
123
|
+
|
|
124
|
+
// Update .env.example and create .env
|
|
125
|
+
const envPath = path.join(targetDir, '.env.example');
|
|
126
|
+
let envContent = await fs.readFile(envPath, 'utf8');
|
|
127
|
+
envContent = envContent.replace('SERVICE_NAME=your-service-name', `SERVICE_NAME=${config.serviceName}`);
|
|
128
|
+
envContent = envContent.replace('PORT=3000', `PORT=${config.port}`);
|
|
129
|
+
|
|
130
|
+
// Set MongoDB URI if provided
|
|
131
|
+
if (config.mongodbUri) {
|
|
132
|
+
envContent = envContent.replace(
|
|
133
|
+
'MONGODB_URI=mongodb+srv://<username>:<password>@<cluster>.mongodb.net/<database>?retryWrites=true&w=majority',
|
|
134
|
+
`MONGODB_URI=${config.mongodbUri}`
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Add JWT env vars if auth selected
|
|
139
|
+
if (config.features.includes('auth')) {
|
|
140
|
+
envContent += `
|
|
141
|
+
# JWT Authentication
|
|
142
|
+
JWT_SECRET=your-super-secret-key-change-in-production
|
|
143
|
+
JWT_EXPIRES_IN=7d
|
|
144
|
+
`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
await fs.writeFile(envPath, envContent);
|
|
148
|
+
await fs.copy(envPath, path.join(targetDir, '.env'));
|
|
149
|
+
|
|
150
|
+
// Add auth middleware if selected
|
|
151
|
+
if (config.features.includes('auth')) {
|
|
152
|
+
await createAuthMiddleware(targetDir);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Add swagger if selected
|
|
156
|
+
if (config.features.includes('swagger')) {
|
|
157
|
+
await createSwaggerSetup(targetDir);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
spinner.succeed('Project structure created');
|
|
161
|
+
|
|
162
|
+
// Initialize git
|
|
163
|
+
if (!options.skipGit) {
|
|
164
|
+
const gitSpinner = ora('Initializing git repository...').start();
|
|
165
|
+
try {
|
|
166
|
+
execSync('git init', { cwd: targetDir, stdio: 'ignore' });
|
|
167
|
+
execSync('git add -A', { cwd: targetDir, stdio: 'ignore' });
|
|
168
|
+
execSync('git commit -m "Initial commit from @listablelabs/create-api"', {
|
|
169
|
+
cwd: targetDir,
|
|
170
|
+
stdio: 'ignore',
|
|
171
|
+
});
|
|
172
|
+
gitSpinner.succeed('Git repository initialized');
|
|
173
|
+
} catch (err) {
|
|
174
|
+
gitSpinner.warn('Git initialization failed (git may not be installed)');
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Install dependencies
|
|
179
|
+
if (!options.skipInstall) {
|
|
180
|
+
const installSpinner = ora('Installing dependencies...').start();
|
|
181
|
+
try {
|
|
182
|
+
execSync('npm install', { cwd: targetDir, stdio: 'ignore' });
|
|
183
|
+
installSpinner.succeed('Dependencies installed');
|
|
184
|
+
} catch (err) {
|
|
185
|
+
installSpinner.fail('Failed to install dependencies. Run npm install manually.');
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Success message
|
|
190
|
+
console.log(chalk.green('\n ✔ Project created successfully!\n'));
|
|
191
|
+
console.log(chalk.white(' Stack: Express + Zod + Pino + MongoDB Atlas\n'));
|
|
192
|
+
console.log(chalk.white(' Next steps:\n'));
|
|
193
|
+
console.log(chalk.cyan(` cd ${validName}`));
|
|
194
|
+
if (options.skipInstall) {
|
|
195
|
+
console.log(chalk.cyan(' npm install'));
|
|
196
|
+
}
|
|
197
|
+
if (!config.mongodbUri) {
|
|
198
|
+
console.log(chalk.yellow(' # Add your MongoDB Atlas URI to .env'));
|
|
199
|
+
}
|
|
200
|
+
console.log(chalk.cyan(' npm run dev\n'));
|
|
201
|
+
console.log(chalk.gray(` Your API will be running at http://localhost:${config.port}`));
|
|
202
|
+
console.log(chalk.gray(' Health check: GET /health'));
|
|
203
|
+
console.log(chalk.gray(' API routes: GET /api/v1/examples\n'));
|
|
204
|
+
|
|
205
|
+
} catch (err) {
|
|
206
|
+
spinner.fail('Failed to create project');
|
|
207
|
+
console.error(chalk.red(err.message));
|
|
208
|
+
process.exit(1);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function createAuthMiddleware(targetDir) {
|
|
213
|
+
const authContent = `const jwt = require('jsonwebtoken');
|
|
214
|
+
const { UnauthorizedError } = require('./errorHandler');
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* JWT Authentication middleware
|
|
218
|
+
* Verifies JWT token from Authorization header
|
|
219
|
+
*/
|
|
220
|
+
const authenticate = (req, res, next) => {
|
|
221
|
+
const authHeader = req.headers.authorization;
|
|
222
|
+
|
|
223
|
+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
224
|
+
throw new UnauthorizedError('No token provided');
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const token = authHeader.split(' ')[1];
|
|
228
|
+
|
|
229
|
+
try {
|
|
230
|
+
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
|
231
|
+
req.user = decoded;
|
|
232
|
+
next();
|
|
233
|
+
} catch (err) {
|
|
234
|
+
throw new UnauthorizedError('Invalid or expired token');
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Optional authentication - sets req.user if valid token exists
|
|
240
|
+
*/
|
|
241
|
+
const optionalAuth = (req, res, next) => {
|
|
242
|
+
const authHeader = req.headers.authorization;
|
|
243
|
+
|
|
244
|
+
if (authHeader && authHeader.startsWith('Bearer ')) {
|
|
245
|
+
const token = authHeader.split(' ')[1];
|
|
246
|
+
try {
|
|
247
|
+
req.user = jwt.verify(token, process.env.JWT_SECRET);
|
|
248
|
+
} catch (err) {
|
|
249
|
+
// Token invalid, but that's okay for optional auth
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
next();
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Generate JWT token
|
|
258
|
+
*/
|
|
259
|
+
const generateToken = (payload) => {
|
|
260
|
+
return jwt.sign(payload, process.env.JWT_SECRET, {
|
|
261
|
+
expiresIn: process.env.JWT_EXPIRES_IN || '7d',
|
|
262
|
+
});
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
module.exports = { authenticate, optionalAuth, generateToken };
|
|
266
|
+
`;
|
|
267
|
+
|
|
268
|
+
await fs.writeFile(path.join(targetDir, 'src', 'middlewares', 'auth.js'), authContent);
|
|
269
|
+
|
|
270
|
+
// Update middleware index
|
|
271
|
+
const indexPath = path.join(targetDir, 'src', 'middlewares', 'index.js');
|
|
272
|
+
let indexContent = await fs.readFile(indexPath, 'utf8');
|
|
273
|
+
indexContent = indexContent.replace(
|
|
274
|
+
"const { defaultLimiter, strictLimiter, createLimiter } = require('./rateLimiter');",
|
|
275
|
+
`const { defaultLimiter, strictLimiter, createLimiter } = require('./rateLimiter');
|
|
276
|
+
const { authenticate, optionalAuth, generateToken } = require('./auth');`
|
|
277
|
+
);
|
|
278
|
+
indexContent = indexContent.replace(
|
|
279
|
+
' createLimiter,\n};',
|
|
280
|
+
` createLimiter,
|
|
281
|
+
|
|
282
|
+
// Authentication
|
|
283
|
+
authenticate,
|
|
284
|
+
optionalAuth,
|
|
285
|
+
generateToken,
|
|
286
|
+
};`
|
|
287
|
+
);
|
|
288
|
+
await fs.writeFile(indexPath, indexContent);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async function createDatabaseConnection(targetDir, dbType) {
|
|
292
|
+
let dbContent;
|
|
293
|
+
|
|
294
|
+
if (dbType === 'postgres') {
|
|
295
|
+
dbContent = `const { Pool } = require('pg');
|
|
296
|
+
const { logger } = require('../utils/logger');
|
|
297
|
+
const config = require('../config');
|
|
298
|
+
|
|
299
|
+
const pool = new Pool({
|
|
300
|
+
host: process.env.DB_HOST,
|
|
301
|
+
port: process.env.DB_PORT,
|
|
302
|
+
database: process.env.DB_NAME,
|
|
303
|
+
user: process.env.DB_USER,
|
|
304
|
+
password: process.env.DB_PASSWORD,
|
|
305
|
+
max: 20, // Max connections in pool
|
|
306
|
+
idleTimeoutMillis: 30000,
|
|
307
|
+
connectionTimeoutMillis: 2000,
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
pool.on('connect', () => {
|
|
311
|
+
logger.debug('New database connection established');
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
pool.on('error', (err) => {
|
|
315
|
+
logger.error({ err }, 'Unexpected database error');
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Query helper with logging
|
|
320
|
+
*/
|
|
321
|
+
const query = async (text, params) => {
|
|
322
|
+
const start = Date.now();
|
|
323
|
+
try {
|
|
324
|
+
const result = await pool.query(text, params);
|
|
325
|
+
const duration = Date.now() - start;
|
|
326
|
+
logger.debug({ query: text, duration, rows: result.rowCount }, 'Database query executed');
|
|
327
|
+
return result;
|
|
328
|
+
} catch (err) {
|
|
329
|
+
logger.error({ err, query: text }, 'Database query failed');
|
|
330
|
+
throw err;
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Get a client from the pool for transactions
|
|
336
|
+
*/
|
|
337
|
+
const getClient = () => pool.connect();
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Health check for database
|
|
341
|
+
*/
|
|
342
|
+
const healthCheck = async () => {
|
|
343
|
+
try {
|
|
344
|
+
await pool.query('SELECT 1');
|
|
345
|
+
return true;
|
|
346
|
+
} catch (err) {
|
|
347
|
+
return false;
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
module.exports = { pool, query, getClient, healthCheck };
|
|
352
|
+
`;
|
|
353
|
+
} else if (dbType === 'mongodb') {
|
|
354
|
+
dbContent = `const mongoose = require('mongoose');
|
|
355
|
+
const { logger } = require('../utils/logger');
|
|
356
|
+
|
|
357
|
+
const connectDB = async () => {
|
|
358
|
+
try {
|
|
359
|
+
const conn = await mongoose.connect(process.env.MONGODB_URI, {
|
|
360
|
+
maxPoolSize: 10,
|
|
361
|
+
});
|
|
362
|
+
logger.info({ host: conn.connection.host }, 'MongoDB connected');
|
|
363
|
+
} catch (err) {
|
|
364
|
+
logger.fatal({ err }, 'MongoDB connection failed');
|
|
365
|
+
process.exit(1);
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
mongoose.connection.on('disconnected', () => {
|
|
370
|
+
logger.warn('MongoDB disconnected');
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
mongoose.connection.on('error', (err) => {
|
|
374
|
+
logger.error({ err }, 'MongoDB error');
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Health check for database
|
|
379
|
+
*/
|
|
380
|
+
const healthCheck = async () => {
|
|
381
|
+
return mongoose.connection.readyState === 1;
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
module.exports = { connectDB, healthCheck };
|
|
385
|
+
`;
|
|
386
|
+
} else if (dbType === 'mysql') {
|
|
387
|
+
dbContent = `const mysql = require('mysql2/promise');
|
|
388
|
+
const { logger } = require('../utils/logger');
|
|
389
|
+
|
|
390
|
+
const pool = mysql.createPool({
|
|
391
|
+
host: process.env.DB_HOST,
|
|
392
|
+
port: process.env.DB_PORT,
|
|
393
|
+
database: process.env.DB_NAME,
|
|
394
|
+
user: process.env.DB_USER,
|
|
395
|
+
password: process.env.DB_PASSWORD,
|
|
396
|
+
waitForConnections: true,
|
|
397
|
+
connectionLimit: 20,
|
|
398
|
+
queueLimit: 0,
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Query helper with logging
|
|
403
|
+
*/
|
|
404
|
+
const query = async (sql, params) => {
|
|
405
|
+
const start = Date.now();
|
|
406
|
+
try {
|
|
407
|
+
const [rows] = await pool.execute(sql, params);
|
|
408
|
+
const duration = Date.now() - start;
|
|
409
|
+
logger.debug({ query: sql, duration }, 'Database query executed');
|
|
410
|
+
return rows;
|
|
411
|
+
} catch (err) {
|
|
412
|
+
logger.error({ err, query: sql }, 'Database query failed');
|
|
413
|
+
throw err;
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Health check for database
|
|
419
|
+
*/
|
|
420
|
+
const healthCheck = async () => {
|
|
421
|
+
try {
|
|
422
|
+
await pool.query('SELECT 1');
|
|
423
|
+
return true;
|
|
424
|
+
} catch (err) {
|
|
425
|
+
return false;
|
|
426
|
+
}
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
module.exports = { pool, query, healthCheck };
|
|
430
|
+
`;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
await fs.ensureDir(path.join(targetDir, 'src', 'database'));
|
|
434
|
+
await fs.writeFile(path.join(targetDir, 'src', 'database', 'index.js'), dbContent);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
async function createSwaggerSetup(targetDir) {
|
|
438
|
+
const swaggerContent = `const swaggerJsdoc = require('swagger-jsdoc');
|
|
439
|
+
const swaggerUi = require('swagger-ui-express');
|
|
440
|
+
const config = require('../config');
|
|
441
|
+
|
|
442
|
+
const options = {
|
|
443
|
+
definition: {
|
|
444
|
+
openapi: '3.0.0',
|
|
445
|
+
info: {
|
|
446
|
+
title: \`\${config.serviceName} API\`,
|
|
447
|
+
version: '1.0.0',
|
|
448
|
+
description: \`API documentation for \${config.serviceName}\`,
|
|
449
|
+
},
|
|
450
|
+
servers: [
|
|
451
|
+
{
|
|
452
|
+
url: \`http://localhost:\${config.port}\`,
|
|
453
|
+
description: 'Development server',
|
|
454
|
+
},
|
|
455
|
+
],
|
|
456
|
+
components: {
|
|
457
|
+
securitySchemes: {
|
|
458
|
+
bearerAuth: {
|
|
459
|
+
type: 'http',
|
|
460
|
+
scheme: 'bearer',
|
|
461
|
+
bearerFormat: 'JWT',
|
|
462
|
+
},
|
|
463
|
+
},
|
|
464
|
+
},
|
|
465
|
+
},
|
|
466
|
+
apis: ['./src/routes/**/*.js'],
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
const specs = swaggerJsdoc(options);
|
|
470
|
+
|
|
471
|
+
const setupSwagger = (app) => {
|
|
472
|
+
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(specs));
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
module.exports = { setupSwagger, specs };
|
|
476
|
+
`;
|
|
477
|
+
|
|
478
|
+
await fs.writeFile(path.join(targetDir, 'src', 'utils', 'swagger.js'), swaggerContent);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
module.exports = create;
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-listablelabs-api",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "CLI to scaffold ListableLabs microservices",
|
|
5
|
+
"bin": {
|
|
6
|
+
"create-listablelabs-api": "./bin/cli.js",
|
|
7
|
+
"listablelabs": "./bin/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"templates"
|
|
12
|
+
],
|
|
13
|
+
"keywords": [
|
|
14
|
+
"listablelabs",
|
|
15
|
+
"microservice",
|
|
16
|
+
"api",
|
|
17
|
+
"scaffold",
|
|
18
|
+
"cli",
|
|
19
|
+
"express",
|
|
20
|
+
"mongodb",
|
|
21
|
+
"zod"
|
|
22
|
+
],
|
|
23
|
+
"author": "ListableLabs",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"chalk": "^4.1.2",
|
|
27
|
+
"commander": "^11.1.0",
|
|
28
|
+
"fs-extra": "^11.2.0",
|
|
29
|
+
"inquirer": "^8.2.6",
|
|
30
|
+
"ora": "^5.4.1"
|
|
31
|
+
},
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=16.0.0"
|
|
34
|
+
},
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "https://github.com/listablelabs/create-api.git"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Application
|
|
2
|
+
NODE_ENV=development
|
|
3
|
+
PORT=3000
|
|
4
|
+
SERVICE_NAME=your-service-name
|
|
5
|
+
|
|
6
|
+
# Logging
|
|
7
|
+
LOG_LEVEL=info
|
|
8
|
+
|
|
9
|
+
# Rate Limiting
|
|
10
|
+
RATE_LIMIT_WINDOW_MS=60000
|
|
11
|
+
RATE_LIMIT_MAX_REQUESTS=100
|
|
12
|
+
|
|
13
|
+
# MongoDB Atlas
|
|
14
|
+
MONGODB_URI=mongodb+srv://<username>:<password>@<cluster>.mongodb.net/<database>?retryWrites=true&w=majority
|
|
15
|
+
|
|
16
|
+
# JWT (uncomment and configure as needed)
|
|
17
|
+
# JWT_SECRET=your-super-secret-key
|
|
18
|
+
# JWT_EXPIRES_IN=1d
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
env: {
|
|
3
|
+
node: true,
|
|
4
|
+
es2022: true,
|
|
5
|
+
jest: true,
|
|
6
|
+
},
|
|
7
|
+
extends: ['eslint:recommended'],
|
|
8
|
+
parserOptions: {
|
|
9
|
+
ecmaVersion: 'latest',
|
|
10
|
+
sourceType: 'module',
|
|
11
|
+
},
|
|
12
|
+
rules: {
|
|
13
|
+
// Error prevention
|
|
14
|
+
'no-console': 'warn', // Use logger instead
|
|
15
|
+
'no-unused-vars': ['error', { argsIgnorePattern: '^_|next' }],
|
|
16
|
+
'no-return-await': 'error',
|
|
17
|
+
|
|
18
|
+
// Style consistency
|
|
19
|
+
'semi': ['error', 'always'],
|
|
20
|
+
'quotes': ['error', 'single', { avoidEscape: true }],
|
|
21
|
+
'comma-dangle': ['error', 'always-multiline'],
|
|
22
|
+
'indent': ['error', 2],
|
|
23
|
+
|
|
24
|
+
// Best practices
|
|
25
|
+
'eqeqeq': ['error', 'always'],
|
|
26
|
+
'curly': ['error', 'all'],
|
|
27
|
+
'no-var': 'error',
|
|
28
|
+
'prefer-const': 'error',
|
|
29
|
+
'prefer-arrow-callback': 'error',
|
|
30
|
+
},
|
|
31
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Build stage
|
|
2
|
+
FROM node:20-alpine AS builder
|
|
3
|
+
|
|
4
|
+
WORKDIR /app
|
|
5
|
+
|
|
6
|
+
# Copy package files
|
|
7
|
+
COPY package*.json ./
|
|
8
|
+
|
|
9
|
+
# Install dependencies (including devDependencies for build)
|
|
10
|
+
RUN npm ci
|
|
11
|
+
|
|
12
|
+
# Copy source code
|
|
13
|
+
COPY . .
|
|
14
|
+
|
|
15
|
+
# Production stage
|
|
16
|
+
FROM node:20-alpine AS production
|
|
17
|
+
|
|
18
|
+
# Create non-root user for security
|
|
19
|
+
RUN addgroup -g 1001 -S nodejs && \
|
|
20
|
+
adduser -S nodejs -u 1001
|
|
21
|
+
|
|
22
|
+
WORKDIR /app
|
|
23
|
+
|
|
24
|
+
# Copy package files
|
|
25
|
+
COPY package*.json ./
|
|
26
|
+
|
|
27
|
+
# Install only production dependencies
|
|
28
|
+
RUN npm ci --only=production && \
|
|
29
|
+
npm cache clean --force
|
|
30
|
+
|
|
31
|
+
# Copy application code
|
|
32
|
+
COPY --from=builder /app/src ./src
|
|
33
|
+
|
|
34
|
+
# Set ownership to non-root user
|
|
35
|
+
RUN chown -R nodejs:nodejs /app
|
|
36
|
+
|
|
37
|
+
# Switch to non-root user
|
|
38
|
+
USER nodejs
|
|
39
|
+
|
|
40
|
+
# Expose port
|
|
41
|
+
EXPOSE 3000
|
|
42
|
+
|
|
43
|
+
# Health check
|
|
44
|
+
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
|
45
|
+
CMD node -e "require('http').get('http://localhost:3000/health', (r) => process.exit(r.statusCode === 200 ? 0 : 1))"
|
|
46
|
+
|
|
47
|
+
# Start the application
|
|
48
|
+
CMD ["node", "src/server.js"]
|