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.
Files changed (32) hide show
  1. package/README.md +142 -0
  2. package/bin/cli.js +37 -0
  3. package/bin/commands/add.js +460 -0
  4. package/bin/commands/create.js +481 -0
  5. package/package.json +39 -0
  6. package/templates/base/.dockerignore +19 -0
  7. package/templates/base/.env.example +18 -0
  8. package/templates/base/.eslintrc.js +31 -0
  9. package/templates/base/Dockerfile +48 -0
  10. package/templates/base/README.md +295 -0
  11. package/templates/base/docker-compose.yml +55 -0
  12. package/templates/base/jest.config.js +24 -0
  13. package/templates/base/package.json +41 -0
  14. package/templates/base/src/app.js +103 -0
  15. package/templates/base/src/config/index.js +36 -0
  16. package/templates/base/src/controllers/exampleController.js +148 -0
  17. package/templates/base/src/database/baseModel.js +160 -0
  18. package/templates/base/src/database/index.js +108 -0
  19. package/templates/base/src/middlewares/errorHandler.js +155 -0
  20. package/templates/base/src/middlewares/index.js +49 -0
  21. package/templates/base/src/middlewares/rateLimiter.js +85 -0
  22. package/templates/base/src/middlewares/requestLogger.js +50 -0
  23. package/templates/base/src/middlewares/validator.js +107 -0
  24. package/templates/base/src/models/example.js +117 -0
  25. package/templates/base/src/models/index.js +6 -0
  26. package/templates/base/src/routes/v1/exampleRoutes.js +89 -0
  27. package/templates/base/src/routes/v1/index.js +19 -0
  28. package/templates/base/src/server.js +80 -0
  29. package/templates/base/src/utils/logger.js +61 -0
  30. package/templates/base/src/utils/response.js +117 -0
  31. package/templates/base/tests/app.test.js +215 -0
  32. package/templates/base/tests/setup.js +33 -0
package/README.md ADDED
@@ -0,0 +1,142 @@
1
+ # @listablelabs/create-api
2
+
3
+ CLI tool to scaffold standardized ListableLabs microservices.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ # Install globally
9
+ npm install -g @listablelabs/create-api
10
+
11
+ # Or use npx (no install required)
12
+ npx @listablelabs/create-api create my-service
13
+ ```
14
+
15
+ ## Usage
16
+
17
+ ### Create a New Service
18
+
19
+ ```bash
20
+ # Interactive mode (recommended)
21
+ listablelabs create my-service
22
+
23
+ # Quick mode with defaults
24
+ listablelabs create my-service -y
25
+
26
+ # Specify directory
27
+ listablelabs create my-service -d ./services
28
+
29
+ # Skip npm install
30
+ listablelabs create my-service --skip-install
31
+
32
+ # Skip git initialization
33
+ listablelabs create my-service --skip-git
34
+ ```
35
+
36
+ ### Add Components to Existing Project
37
+
38
+ ```bash
39
+ # Add a new route + controller
40
+ listablelabs add route
41
+ listablelabs add route -n users
42
+
43
+ # Add just a controller
44
+ listablelabs add controller -n payment
45
+
46
+ # Add a service
47
+ listablelabs add service -n email
48
+
49
+ # Add a middleware
50
+ listablelabs add middleware -n cache
51
+
52
+ # Add a model (auto-detects MongoDB/SQL)
53
+ listablelabs add model -n user
54
+ ```
55
+
56
+ ## Features
57
+
58
+ When creating a new service, you can choose:
59
+
60
+ - **Logging** - Pino with pretty printing in dev
61
+ - **Validation** - Joi request validation
62
+ - **Rate Limiting** - Express rate limit
63
+ - **Error Handling** - Centralized error handling with typed errors
64
+ - **JWT Authentication** - Optional auth middleware
65
+ - **Swagger/OpenAPI** - Optional API documentation
66
+ - **Database** - PostgreSQL, MongoDB, or MySQL support
67
+
68
+ ## Project Structure
69
+
70
+ ```
71
+ my-service/
72
+ ├── src/
73
+ │ ├── config/ # Environment configuration
74
+ │ ├── controllers/ # Request handlers
75
+ │ ├── middlewares/ # Express middlewares
76
+ │ ├── routes/v1/ # API routes
77
+ │ ├── services/ # Business logic
78
+ │ ├── models/ # Data models
79
+ │ ├── utils/ # Utilities (logger, response)
80
+ │ ├── app.js # Express setup
81
+ │ └── server.js # Entry point
82
+ ├── tests/
83
+ ├── Dockerfile
84
+ └── docker-compose.yml
85
+ ```
86
+
87
+ ## Publishing (Internal)
88
+
89
+ ```bash
90
+ # Login to npm (or your private registry)
91
+ npm login
92
+
93
+ # Publish
94
+ npm publish --access public
95
+ ```
96
+
97
+ ## Examples
98
+
99
+ ### Create a user service with PostgreSQL and auth:
100
+
101
+ ```bash
102
+ $ listablelabs create user-service
103
+
104
+ ? Service name: user-service
105
+ ? Description: User management microservice
106
+ ? Port: 3001
107
+ ? Select features:
108
+ ◉ Pino Logging
109
+ ◉ Joi Validation
110
+ ◉ Rate Limiting
111
+ ◉ Error Handling
112
+ ◉ JWT Authentication
113
+ ◯ Swagger/OpenAPI Docs
114
+ ? Database: PostgreSQL
115
+
116
+ ✔ Project structure created
117
+ ✔ Git repository initialized
118
+ ✔ Dependencies installed
119
+
120
+ ✔ Project created successfully!
121
+
122
+ Next steps:
123
+ cd user-service
124
+ npm run dev
125
+ ```
126
+
127
+ ### Add a products route:
128
+
129
+ ```bash
130
+ $ cd user-service
131
+ $ listablelabs add route -n products
132
+
133
+ ✔ Created products route and controller
134
+
135
+ Files created:
136
+ - src/routes/v1/productsRoutes.js
137
+ - src/controllers/productsController.js
138
+
139
+ Add to src/routes/v1/index.js:
140
+ const productsRoutes = require('./productsRoutes');
141
+ router.use('/products', productsRoutes);
142
+ ```
package/bin/cli.js ADDED
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { program } = require('commander');
4
+ const chalk = require('chalk');
5
+ const packageJson = require('../package.json');
6
+
7
+ program
8
+ .name('listablelabs')
9
+ .description('CLI to scaffold ListableLabs microservices')
10
+ .version(packageJson.version);
11
+
12
+ program
13
+ .command('create <project-name>')
14
+ .description('Create a new ListableLabs API service')
15
+ .option('-d, --directory <dir>', 'Directory to create project in', '.')
16
+ .option('--skip-install', 'Skip npm install')
17
+ .option('--skip-git', 'Skip git initialization')
18
+ .option('-y, --yes', 'Skip prompts and use defaults')
19
+ .action(require('./commands/create'));
20
+
21
+ program
22
+ .command('add <component>')
23
+ .description('Add a component to existing project (route, controller, model)')
24
+ .option('-n, --name <name>', 'Name of the component')
25
+ .action(require('./commands/add'));
26
+
27
+ program.parse();
28
+
29
+ // Show help if no command provided
30
+ if (!process.argv.slice(2).length) {
31
+ console.log(chalk.cyan(`
32
+ ╦ ╦╔═╗╔╦╗╔═╗╔╗ ╦ ╔═╗╦ ╔═╗╔╗ ╔═╗
33
+ ║ ║╚═╗ ║ ╠═╣╠╩╗║ ║╣ ║ ╠═╣╠╩╗╚═╗
34
+ ╩═╝╩╚═╝ ╩ ╩ ╩╚═╝╩═╝╚═╝╩═╝╩ ╩╚═╝╚═╝
35
+ `));
36
+ program.help();
37
+ }
@@ -0,0 +1,460 @@
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
+
7
+ async function add(component, options) {
8
+ // Check if we're in a listablelabs project
9
+ const pkgPath = path.join(process.cwd(), 'package.json');
10
+ if (!fs.existsSync(pkgPath)) {
11
+ console.log(chalk.red(' Error: Not in a Node.js project directory'));
12
+ process.exit(1);
13
+ }
14
+
15
+ const pkg = await fs.readJson(pkgPath);
16
+ if (!pkg.name || !pkg.name.includes('@listablelabs')) {
17
+ console.log(chalk.yellow(' Warning: This may not be a ListableLabs project'));
18
+ }
19
+
20
+ const componentType = component.toLowerCase();
21
+
22
+ switch (componentType) {
23
+ case 'route':
24
+ case 'routes':
25
+ await addRoute(options);
26
+ break;
27
+ case 'controller':
28
+ await addController(options);
29
+ break;
30
+ case 'model':
31
+ await addModel(options);
32
+ break;
33
+ case 'middleware':
34
+ await addMiddleware(options);
35
+ break;
36
+ case 'service':
37
+ await addService(options);
38
+ break;
39
+ default:
40
+ console.log(chalk.red(` Unknown component type: ${component}`));
41
+ console.log(chalk.gray(' Available: route, controller, model, middleware, service'));
42
+ process.exit(1);
43
+ }
44
+ }
45
+
46
+ async function addRoute(options) {
47
+ let name = options.name;
48
+
49
+ if (!name) {
50
+ const answers = await inquirer.prompt([
51
+ {
52
+ type: 'input',
53
+ name: 'name',
54
+ message: 'Route name (e.g., users, products):',
55
+ validate: (input) => input.length > 0 || 'Name is required',
56
+ },
57
+ ]);
58
+ name = answers.name;
59
+ }
60
+
61
+ const routeName = name.toLowerCase().replace(/[^a-z0-9]/g, '');
62
+ const routeDir = path.join(process.cwd(), 'src', 'routes', 'v1');
63
+ const controllerDir = path.join(process.cwd(), 'src', 'controllers');
64
+
65
+ // Ensure directories exist
66
+ await fs.ensureDir(routeDir);
67
+ await fs.ensureDir(controllerDir);
68
+
69
+ const spinner = ora(`Creating ${routeName} route...`).start();
70
+
71
+ // Create route file
72
+ const routeContent = `const { Router } = require('express');
73
+ const ${routeName}Controller = require('../../controllers/${routeName}Controller');
74
+ const { validate, Joi } = require('../../middlewares');
75
+
76
+ const router = Router();
77
+
78
+ /**
79
+ * Validation schemas
80
+ */
81
+ const schemas = {
82
+ getById: {
83
+ params: Joi.object({
84
+ id: Joi.string().required(),
85
+ }),
86
+ },
87
+ create: {
88
+ body: Joi.object({
89
+ // Add your fields here
90
+ name: Joi.string().min(1).max(255).required(),
91
+ }),
92
+ },
93
+ update: {
94
+ params: Joi.object({
95
+ id: Joi.string().required(),
96
+ }),
97
+ body: Joi.object({
98
+ name: Joi.string().min(1).max(255),
99
+ }),
100
+ },
101
+ };
102
+
103
+ /**
104
+ * @route GET /api/v1/${routeName}
105
+ * @desc Get all ${routeName}
106
+ */
107
+ router.get('/', ${routeName}Controller.getAll);
108
+
109
+ /**
110
+ * @route GET /api/v1/${routeName}/:id
111
+ * @desc Get ${routeName} by ID
112
+ */
113
+ router.get('/:id', validate(schemas.getById), ${routeName}Controller.getById);
114
+
115
+ /**
116
+ * @route POST /api/v1/${routeName}
117
+ * @desc Create new ${routeName}
118
+ */
119
+ router.post('/', validate(schemas.create), ${routeName}Controller.create);
120
+
121
+ /**
122
+ * @route PUT /api/v1/${routeName}/:id
123
+ * @desc Update ${routeName}
124
+ */
125
+ router.put('/:id', validate(schemas.update), ${routeName}Controller.update);
126
+
127
+ /**
128
+ * @route DELETE /api/v1/${routeName}/:id
129
+ * @desc Delete ${routeName}
130
+ */
131
+ router.delete('/:id', validate(schemas.getById), ${routeName}Controller.remove);
132
+
133
+ module.exports = router;
134
+ `;
135
+
136
+ // Create controller file
137
+ const controllerContent = `const { asyncHandler, NotFoundError } = require('../middlewares');
138
+ const { sendSuccess, sendCreated, sendPaginated, sendNoContent } = require('../utils/response');
139
+ const { createChildLogger } = require('../utils/logger');
140
+
141
+ const log = createChildLogger({ module: '${routeName}Controller' });
142
+
143
+ /**
144
+ * Get all ${routeName}
145
+ */
146
+ const getAll = asyncHandler(async (req, res) => {
147
+ const { page = 1, limit = 20 } = req.query;
148
+ log.info({ page, limit }, 'Fetching ${routeName}');
149
+
150
+ // TODO: Replace with actual database query
151
+ const items = [];
152
+ const total = 0;
153
+
154
+ sendPaginated(res, items, { page: +page, limit: +limit, total });
155
+ });
156
+
157
+ /**
158
+ * Get ${routeName} by ID
159
+ */
160
+ const getById = asyncHandler(async (req, res) => {
161
+ const { id } = req.params;
162
+ log.info({ id }, 'Fetching ${routeName} by ID');
163
+
164
+ // TODO: Replace with actual database query
165
+ const item = null;
166
+
167
+ if (!item) {
168
+ throw new NotFoundError(\`${routeName} with ID \${id} not found\`);
169
+ }
170
+
171
+ sendSuccess(res, item);
172
+ });
173
+
174
+ /**
175
+ * Create new ${routeName}
176
+ */
177
+ const create = asyncHandler(async (req, res) => {
178
+ const data = req.body;
179
+ log.info({ data }, 'Creating ${routeName}');
180
+
181
+ // TODO: Replace with actual database insert
182
+ const newItem = { id: '1', ...data, createdAt: new Date() };
183
+
184
+ sendCreated(res, newItem);
185
+ });
186
+
187
+ /**
188
+ * Update ${routeName}
189
+ */
190
+ const update = asyncHandler(async (req, res) => {
191
+ const { id } = req.params;
192
+ const data = req.body;
193
+ log.info({ id, data }, 'Updating ${routeName}');
194
+
195
+ // TODO: Replace with actual database update
196
+ const updatedItem = { id, ...data, updatedAt: new Date() };
197
+
198
+ sendSuccess(res, updatedItem, 200, '${routeName} updated');
199
+ });
200
+
201
+ /**
202
+ * Delete ${routeName}
203
+ */
204
+ const remove = asyncHandler(async (req, res) => {
205
+ const { id } = req.params;
206
+ log.info({ id }, 'Deleting ${routeName}');
207
+
208
+ // TODO: Replace with actual database delete
209
+
210
+ sendNoContent(res);
211
+ });
212
+
213
+ module.exports = {
214
+ getAll,
215
+ getById,
216
+ create,
217
+ update,
218
+ remove,
219
+ };
220
+ `;
221
+
222
+ await fs.writeFile(path.join(routeDir, `${routeName}Routes.js`), routeContent);
223
+ await fs.writeFile(path.join(controllerDir, `${routeName}Controller.js`), controllerContent);
224
+
225
+ spinner.succeed(`Created ${routeName} route and controller`);
226
+
227
+ console.log(chalk.gray(`
228
+ Files created:
229
+ - src/routes/v1/${routeName}Routes.js
230
+ - src/controllers/${routeName}Controller.js
231
+
232
+ Add to src/routes/v1/index.js:
233
+ `));
234
+ console.log(chalk.cyan(` const ${routeName}Routes = require('./${routeName}Routes');`));
235
+ console.log(chalk.cyan(` router.use('/${routeName}', ${routeName}Routes);`));
236
+ }
237
+
238
+ async function addController(options) {
239
+ let name = options.name;
240
+
241
+ if (!name) {
242
+ const answers = await inquirer.prompt([
243
+ {
244
+ type: 'input',
245
+ name: 'name',
246
+ message: 'Controller name:',
247
+ validate: (input) => input.length > 0 || 'Name is required',
248
+ },
249
+ ]);
250
+ name = answers.name;
251
+ }
252
+
253
+ const controllerName = name.toLowerCase().replace(/[^a-z0-9]/g, '');
254
+ const controllerDir = path.join(process.cwd(), 'src', 'controllers');
255
+ await fs.ensureDir(controllerDir);
256
+
257
+ const spinner = ora(`Creating ${controllerName} controller...`).start();
258
+
259
+ const content = `const { asyncHandler, NotFoundError } = require('../middlewares');
260
+ const { sendSuccess } = require('../utils/response');
261
+ const { createChildLogger } = require('../utils/logger');
262
+
263
+ const log = createChildLogger({ module: '${controllerName}Controller' });
264
+
265
+ // Add your controller methods here
266
+
267
+ module.exports = {
268
+ // export methods
269
+ };
270
+ `;
271
+
272
+ await fs.writeFile(path.join(controllerDir, `${controllerName}Controller.js`), content);
273
+ spinner.succeed(`Created ${controllerName}Controller.js`);
274
+ }
275
+
276
+ async function addService(options) {
277
+ let name = options.name;
278
+
279
+ if (!name) {
280
+ const answers = await inquirer.prompt([
281
+ {
282
+ type: 'input',
283
+ name: 'name',
284
+ message: 'Service name:',
285
+ validate: (input) => input.length > 0 || 'Name is required',
286
+ },
287
+ ]);
288
+ name = answers.name;
289
+ }
290
+
291
+ const serviceName = name.toLowerCase().replace(/[^a-z0-9]/g, '');
292
+ const serviceDir = path.join(process.cwd(), 'src', 'services');
293
+ await fs.ensureDir(serviceDir);
294
+
295
+ const spinner = ora(`Creating ${serviceName} service...`).start();
296
+
297
+ const content = `const { createChildLogger } = require('../utils/logger');
298
+
299
+ const log = createChildLogger({ module: '${serviceName}Service' });
300
+
301
+ /**
302
+ * ${serviceName} service
303
+ * Business logic layer
304
+ */
305
+ class ${serviceName.charAt(0).toUpperCase() + serviceName.slice(1)}Service {
306
+ // Add your service methods here
307
+ }
308
+
309
+ module.exports = new ${serviceName.charAt(0).toUpperCase() + serviceName.slice(1)}Service();
310
+ `;
311
+
312
+ await fs.writeFile(path.join(serviceDir, `${serviceName}Service.js`), content);
313
+ spinner.succeed(`Created ${serviceName}Service.js`);
314
+ }
315
+
316
+ async function addMiddleware(options) {
317
+ let name = options.name;
318
+
319
+ if (!name) {
320
+ const answers = await inquirer.prompt([
321
+ {
322
+ type: 'input',
323
+ name: 'name',
324
+ message: 'Middleware name:',
325
+ validate: (input) => input.length > 0 || 'Name is required',
326
+ },
327
+ ]);
328
+ name = answers.name;
329
+ }
330
+
331
+ const middlewareName = name.toLowerCase().replace(/[^a-z0-9]/g, '');
332
+ const middlewareDir = path.join(process.cwd(), 'src', 'middlewares');
333
+
334
+ const spinner = ora(`Creating ${middlewareName} middleware...`).start();
335
+
336
+ const content = `const { createChildLogger } = require('../utils/logger');
337
+
338
+ const log = createChildLogger({ module: '${middlewareName}Middleware' });
339
+
340
+ /**
341
+ * ${middlewareName} middleware
342
+ */
343
+ const ${middlewareName} = (req, res, next) => {
344
+ // Add your middleware logic here
345
+ next();
346
+ };
347
+
348
+ module.exports = { ${middlewareName} };
349
+ `;
350
+
351
+ await fs.writeFile(path.join(middlewareDir, `${middlewareName}.js`), content);
352
+ spinner.succeed(`Created ${middlewareName}.js middleware`);
353
+
354
+ console.log(chalk.gray(`
355
+ Don't forget to export in src/middlewares/index.js
356
+ `));
357
+ }
358
+
359
+ async function addModel(options) {
360
+ let name = options.name;
361
+
362
+ if (!name) {
363
+ const answers = await inquirer.prompt([
364
+ {
365
+ type: 'input',
366
+ name: 'name',
367
+ message: 'Model name:',
368
+ validate: (input) => input.length > 0 || 'Name is required',
369
+ },
370
+ ]);
371
+ name = answers.name;
372
+ }
373
+
374
+ const modelName = name.toLowerCase().replace(/[^a-z0-9]/g, '');
375
+ const modelDir = path.join(process.cwd(), 'src', 'models');
376
+ await fs.ensureDir(modelDir);
377
+
378
+ // Check if mongoose is installed (MongoDB)
379
+ const pkgPath = path.join(process.cwd(), 'package.json');
380
+ const pkg = await fs.readJson(pkgPath);
381
+ const isMongoose = pkg.dependencies && pkg.dependencies.mongoose;
382
+
383
+ const spinner = ora(`Creating ${modelName} model...`).start();
384
+
385
+ let content;
386
+
387
+ if (isMongoose) {
388
+ const className = modelName.charAt(0).toUpperCase() + modelName.slice(1);
389
+ content = `const mongoose = require('mongoose');
390
+
391
+ const ${modelName}Schema = new mongoose.Schema(
392
+ {
393
+ name: {
394
+ type: String,
395
+ required: true,
396
+ trim: true,
397
+ },
398
+ // Add more fields here
399
+ },
400
+ {
401
+ timestamps: true,
402
+ }
403
+ );
404
+
405
+ // Add indexes
406
+ ${modelName}Schema.index({ name: 1 });
407
+
408
+ // Add methods
409
+ ${modelName}Schema.methods.toJSON = function () {
410
+ const obj = this.toObject();
411
+ delete obj.__v;
412
+ return obj;
413
+ };
414
+
415
+ module.exports = mongoose.model('${className}', ${modelName}Schema);
416
+ `;
417
+ } else {
418
+ content = `/**
419
+ * ${modelName} model
420
+ * Define your data structure and database interactions here
421
+ */
422
+
423
+ const ${modelName}Model = {
424
+ // Add your model methods here
425
+
426
+ async findAll() {
427
+ // TODO: Implement database query
428
+ return [];
429
+ },
430
+
431
+ async findById(id) {
432
+ // TODO: Implement database query
433
+ return null;
434
+ },
435
+
436
+ async create(data) {
437
+ // TODO: Implement database insert
438
+ return { id: '1', ...data };
439
+ },
440
+
441
+ async update(id, data) {
442
+ // TODO: Implement database update
443
+ return { id, ...data };
444
+ },
445
+
446
+ async delete(id) {
447
+ // TODO: Implement database delete
448
+ return true;
449
+ },
450
+ };
451
+
452
+ module.exports = ${modelName}Model;
453
+ `;
454
+ }
455
+
456
+ await fs.writeFile(path.join(modelDir, `${modelName}.js`), content);
457
+ spinner.succeed(`Created ${modelName} model`);
458
+ }
459
+
460
+ module.exports = add;