create-elseware-nodejs-app 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 ADDED
@@ -0,0 +1 @@
1
+ # create-elseware-app
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+
3
+ import("../dist/index.js");
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/index.js ADDED
@@ -0,0 +1,114 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/create.ts
4
+ import path2 from "path";
5
+ import fs2 from "fs";
6
+
7
+ // src/prompts.ts
8
+ import inquirer from "inquirer";
9
+ async function getProjectOptions(projectNameFromArg) {
10
+ const answers = await inquirer.prompt([
11
+ {
12
+ type: "input",
13
+ name: "name",
14
+ message: "Project name:",
15
+ default: projectNameFromArg,
16
+ validate: (input) => Boolean(input) || "Project name is required"
17
+ },
18
+ {
19
+ type: "input",
20
+ name: "version",
21
+ message: "Initial version:",
22
+ default: "1.0.0"
23
+ },
24
+ {
25
+ type: "input",
26
+ name: "description",
27
+ message: "Project description:",
28
+ default: "Elseware NodeJS application"
29
+ }
30
+ ]);
31
+ return answers;
32
+ }
33
+
34
+ // src/generator.ts
35
+ import fs from "fs";
36
+ import path from "path";
37
+ import { fileURLToPath } from "url";
38
+
39
+ // utils/logger.ts
40
+ var logger = {
41
+ success: (msg) => console.log(`\u2705 ${msg}`),
42
+ info: (msg) => console.log(`\u2139\uFE0F ${msg}`),
43
+ warn: (msg) => console.log(`\u26A0\uFE0F ${msg}`),
44
+ error: (msg) => console.error(`\u274C ${msg}`)
45
+ };
46
+
47
+ // src/generator.ts
48
+ var __filename = fileURLToPath(import.meta.url);
49
+ var __dirname = path.dirname(__filename);
50
+ async function generateProject({ targetDir, options }) {
51
+ const templateDir = path.resolve(__dirname, "../templates/base");
52
+ fs.mkdirSync(targetDir, { recursive: true });
53
+ copyRecursive(templateDir, targetDir);
54
+ updatePackageJson(targetDir, options);
55
+ }
56
+ function copyRecursive(src, dest) {
57
+ for (const entry of fs.readdirSync(src)) {
58
+ const srcPath = path.join(src, entry);
59
+ const destPath = path.join(dest, entry);
60
+ if (fs.statSync(srcPath).isDirectory()) {
61
+ fs.mkdirSync(destPath, { recursive: true });
62
+ copyRecursive(srcPath, destPath);
63
+ } else {
64
+ fs.copyFileSync(srcPath, destPath);
65
+ }
66
+ }
67
+ }
68
+ function updatePackageJson(targetDir, options) {
69
+ const pkgPath = path.join(targetDir, "package.json");
70
+ if (!fs.existsSync(pkgPath)) {
71
+ throw new Error("package.json not found in template");
72
+ }
73
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
74
+ pkg.name = options.name;
75
+ pkg.version = options.version;
76
+ pkg.description = options.description;
77
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
78
+ logger.success("package.json configured");
79
+ }
80
+
81
+ // src/create.ts
82
+ async function createProject(projectName2) {
83
+ const options = await getProjectOptions(projectName2);
84
+ const targetDir = path2.resolve(process.cwd(), options.name);
85
+ if (fs2.existsSync(targetDir)) {
86
+ throw new Error(`Directory "${options.name}" already exists`);
87
+ }
88
+ logger.info(`Creating Elseware app: ${options.name}`);
89
+ await generateProject({
90
+ targetDir,
91
+ options
92
+ });
93
+ logger.success("Project created successfully!");
94
+ logger.info("Next steps:");
95
+ logger.info(` cd ${options.name}`);
96
+ logger.info(" npm install");
97
+ logger.info(" npm run dev");
98
+ }
99
+
100
+ // src/index.ts
101
+ var [, , projectName] = process.argv;
102
+ async function bootstrap() {
103
+ try {
104
+ await createProject(projectName);
105
+ return;
106
+ } catch (error) {
107
+ logger.error("Failed to execute command");
108
+ if (error instanceof Error) {
109
+ logger.error(error.message);
110
+ }
111
+ process.exit(1);
112
+ }
113
+ }
114
+ bootstrap();
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "create-elseware-nodejs-app",
3
+ "version": "1.0.0",
4
+ "description": "CLI to scaffold Elseware NodeJS applications",
5
+ "type": "module",
6
+ "private": false,
7
+ "bin": {
8
+ "create-elseware-nodejs-app": "bin/elseware.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "bin",
13
+ "templates"
14
+ ],
15
+ "scripts": {
16
+ "clean": "rm -rf dist",
17
+ "build": "tsup src/index.ts --format esm --dts",
18
+ "dev": "tsx src/index.ts",
19
+ "prepublishOnly": "npm run build",
20
+ "sync-template": "tsx src/sync-template.ts",
21
+ "release": "npm publish --access public"
22
+ },
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/elsewaretechnologies/elseware-npm-cli.git"
26
+ },
27
+ "author": "Dhanushka Sandakelum",
28
+ "license": "ISC",
29
+ "bugs": {
30
+ "url": "https://github.com/elsewaretechnologies/elseware-npm-cli/issues"
31
+ },
32
+ "homepage": "https://github.com/elsewaretechnologies/elseware-npm-cli#readme",
33
+ "dependencies": {
34
+ "inquirer": "^12.11.1"
35
+ },
36
+ "devDependencies": {
37
+ "@types/node": "^25.0.3",
38
+ "tsup": "^8.0.0",
39
+ "tsx": "^4.19.0",
40
+ "typescript": "^5.9.0"
41
+ }
42
+ }
@@ -0,0 +1,7 @@
1
+ NODE_ENV=development
2
+ PORT=4000
3
+
4
+ DATABASE_URL=mongodb://localhost:27017/elseware-nodejs-template-test-db
5
+ JWT_SECRET=super-secret-key
6
+
7
+ LOG_LEVEL=info
@@ -0,0 +1,41 @@
1
+ # elseware-nodejs-template
2
+
3
+ Production-ready Node.js REST API template.
4
+
5
+ ## Features
6
+
7
+ - Express + ESM
8
+ - Clean architecture
9
+ - Jest testing
10
+ - Env validation
11
+ - Scalable structure
12
+
13
+ ## Run
14
+
15
+ npm install
16
+ npm run dev
17
+
18
+ ## Test
19
+
20
+ npm test
21
+
22
+ ## Architecture
23
+
24
+ ### Hybrid Pattern for APIs
25
+
26
+ To quickly implment generic REST APIs, we can use APIFactory without concerning **service layer**. But for the APIs that require complex **data layer** validations, we can use **service layer**.
27
+
28
+ - Use ApiFactory for “simple CRUD”
29
+ - Use Services for “business logic”
30
+ - Mixing them is not a smell — it’s a strength.
31
+
32
+ ```javascript
33
+ // Simple
34
+ export const getUsers = ApiFactory.getAll(UserModel);
35
+
36
+ // Complex
37
+ export const createUser = asyncHandler(async (req, res) => {
38
+ const user = await userService.createUser(req.body);
39
+ ApiResponse.created(res, user);
40
+ });
41
+ ```
@@ -0,0 +1,73 @@
1
+ import js from "@eslint/js";
2
+ import globals from "globals";
3
+ import jestPlugin from "eslint-plugin-jest";
4
+ import tseslint from "typescript-eslint";
5
+
6
+ export default [
7
+ // Base JS rules (real bugs)
8
+ js.configs.recommended,
9
+
10
+ // TypeScript-aware rules
11
+ ...tseslint.configs.recommended,
12
+
13
+ {
14
+ files: ["**/*.ts"],
15
+ languageOptions: {
16
+ parserOptions: {
17
+ project: "./tsconfig.json",
18
+ tsconfigRootDir: import.meta.dirname,
19
+ },
20
+ globals: {
21
+ ...globals.node,
22
+ ...globals.es2022,
23
+ },
24
+ },
25
+ rules: {
26
+ // =============================
27
+ // Formatting (DISABLED)
28
+ // =============================
29
+ indent: "off",
30
+ "comma-dangle": "off",
31
+ quotes: "off",
32
+ semi: "off",
33
+
34
+ // =============================
35
+ // TypeScript-specific safety
36
+ // =============================
37
+ "@typescript-eslint/no-unused-vars": [
38
+ "warn",
39
+ { argsIgnorePattern: "^_" },
40
+ ],
41
+
42
+ "@typescript-eslint/no-explicit-any": "warn",
43
+
44
+ "@typescript-eslint/consistent-type-imports": [
45
+ "warn",
46
+ { prefer: "type-imports" },
47
+ ],
48
+
49
+ "@typescript-eslint/no-floating-promises": "error",
50
+
51
+ "@typescript-eslint/await-thenable": "error",
52
+
53
+ // =============================
54
+ // Node / backend
55
+ // =============================
56
+ "no-console": "off",
57
+ },
58
+ },
59
+
60
+ // Jest tests
61
+ {
62
+ files: ["**/*.test.ts"],
63
+ plugins: {
64
+ jest: jestPlugin,
65
+ },
66
+ languageOptions: {
67
+ globals: globals.jest,
68
+ },
69
+ rules: {
70
+ "@typescript-eslint/no-explicit-any": "off",
71
+ },
72
+ },
73
+ ];
@@ -0,0 +1,5 @@
1
+ export default {
2
+ testEnvironment: "node",
3
+ verbose: true,
4
+ transform: {},
5
+ };
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "elseware-nodejs-template",
3
+ "version": "1.0.0",
4
+ "description": "Sample elseware App for NodeJs",
5
+ "type": "module",
6
+ "main": "server.ts",
7
+ "scripts": {
8
+ "dev": "tsx watch src/server.ts",
9
+ "build": "tsc",
10
+ "start": "node dist/server.js",
11
+ "test": "NODE_ENV=test node --experimental-vm-modules ./node_modules/.bin/jest",
12
+ "coverage": "NODE_ENV=test node --experimental-vm-modules ./node_modules/.bin/jest --coverage",
13
+ "lint": "eslint .",
14
+ "lint:fix": "eslint . --fix"
15
+ },
16
+ "license": "ISC",
17
+ "dependencies": {
18
+ "dotenv": "^17.2.3",
19
+ "elseware-nodejs": "^1.0.3",
20
+ "express": "^5.2.1",
21
+ "helmet": "^8.1.0",
22
+ "joi": "^18.0.2",
23
+ "mongoose": "^9.0.2",
24
+ "pino": "^10.1.0"
25
+ },
26
+ "devDependencies": {
27
+ "@eslint/js": "^9.39.2",
28
+ "@types/express": "^5.0.6",
29
+ "@types/jest": "^30.0.0",
30
+ "@types/node": "^25.0.3",
31
+ "@types/supertest": "^6.0.3",
32
+ "eslint": "^9.39.2",
33
+ "eslint-config-prettier": "^10.1.8",
34
+ "eslint-plugin-jest": "^28.14.0",
35
+ "globals": "^16.5.0",
36
+ "jest": "^29.7.0",
37
+ "pino-pretty": "^13.1.3",
38
+ "prettier": "^3.7.4",
39
+ "supertest": "^7.1.4",
40
+ "ts-node": "^10.9.2",
41
+ "tsx": "^4.21.0",
42
+ "typescript": "^5.9.3",
43
+ "typescript-eslint": "^8.51.0"
44
+ }
45
+ }
@@ -0,0 +1,24 @@
1
+ import express, { Application } from "express";
2
+ import { AppError, GlobalErrorHandler } from "elseware-nodejs";
3
+ import helmet from "helmet";
4
+
5
+ import routes from "./routes/index.js";
6
+ import { env } from "./configs/env.js";
7
+
8
+ const app: Application = express();
9
+
10
+ app.use(helmet());
11
+
12
+ app.use(express.json());
13
+
14
+ app.use("/api/v1", routes);
15
+
16
+ // Unhandled routes
17
+ app.use((req, _res, next) => {
18
+ next(new AppError(`Route ${req.originalUrl} not found`, 404));
19
+ });
20
+
21
+ // Global error middleware
22
+ app.use(GlobalErrorHandler(env.NODE_ENV === "production"));
23
+
24
+ export default app;
@@ -0,0 +1,4 @@
1
+ import { connectMongoDB } from "elseware-nodejs";
2
+ import { env } from "./env.js";
3
+
4
+ export const connectDatabase = () => connectMongoDB({ uri: env.DATABASE_URL });
@@ -0,0 +1,26 @@
1
+ import Joi from "joi";
2
+ import { loadEnv, logger } from "elseware-nodejs";
3
+
4
+ export interface EnvConfig {
5
+ NODE_ENV: "development" | "production" | "test";
6
+ PORT: number;
7
+ DATABASE_URL: string;
8
+ JWT_SECRET: string;
9
+ LOG_LEVEL: string;
10
+ }
11
+
12
+ const schema = Joi.object<EnvConfig>({
13
+ NODE_ENV: Joi.string()
14
+ .valid("development", "production", "test")
15
+ .default("development"),
16
+ PORT: Joi.number().default(4000),
17
+ DATABASE_URL: Joi.string().required(),
18
+ JWT_SECRET: Joi.string().required(),
19
+ LOG_LEVEL: Joi.string().default("info"),
20
+ }).unknown(true);
21
+
22
+ const value = loadEnv(schema);
23
+
24
+ logger.success("Configurations passed");
25
+
26
+ export const env: EnvConfig = value;
@@ -0,0 +1,14 @@
1
+ import type { Request, Response } from "express";
2
+ import { ApiResponse } from "elseware-nodejs";
3
+
4
+ export const healthCheck = (_req: Request, res: Response): Response => {
5
+ return ApiResponse.ok(
6
+ res,
7
+ {
8
+ status: "ok",
9
+ service: "elseware-nodejs-template",
10
+ timestamp: new Date().toISOString(),
11
+ },
12
+ "Service is healthy"
13
+ );
14
+ };
@@ -0,0 +1,27 @@
1
+ import { APIFactory } from "elseware-nodejs";
2
+
3
+ import { PostModel } from "../models/post.model.js";
4
+
5
+ /**
6
+ * SIMPLE CRUD → APIFactory (no service needed)
7
+ */
8
+ export const getPosts = APIFactory.getAll(PostModel, {
9
+ message: "Posts fetched successfully",
10
+ });
11
+
12
+ export const getPostById = APIFactory.getOne(PostModel, {
13
+ notFoundMessage: "Post not found",
14
+ });
15
+
16
+ export const createPost = APIFactory.createOne(PostModel, {
17
+ message: "Post created successfully",
18
+ });
19
+
20
+ export const updatePost = APIFactory.updateOne(PostModel, {
21
+ message: "Post updated successfully",
22
+ notFoundMessage: "Post not found",
23
+ });
24
+
25
+ export const deletePost = APIFactory.deleteOne(PostModel, {
26
+ notFoundMessage: "Post not found",
27
+ });
@@ -0,0 +1,33 @@
1
+ import { asyncHandler, ApiResponse } from "elseware-nodejs";
2
+
3
+ import { userService } from "../services/user.service.js";
4
+
5
+ /**
6
+ * COMPLEX CRUD → service needed
7
+ */
8
+ export const createUser = asyncHandler(async (req, res) => {
9
+ const user = await userService.create(req.body);
10
+ ApiResponse.created(res, user, "User created successfully");
11
+ });
12
+
13
+ export const getUsers = asyncHandler(async (_req, res) => {
14
+ const users = await userService.findAll();
15
+ ApiResponse.ok(res, users, "Users fetched successfully", {
16
+ count: users.length,
17
+ });
18
+ });
19
+
20
+ export const getUserById = asyncHandler(async (req, res) => {
21
+ const user = await userService.findById(req.params.id);
22
+ ApiResponse.ok(res, user);
23
+ });
24
+
25
+ export const updateUser = asyncHandler(async (req, res) => {
26
+ const user = await userService.updateById(req.params.id, req.body);
27
+ ApiResponse.ok(res, user);
28
+ });
29
+
30
+ export const deleteUser = asyncHandler(async (req, res) => {
31
+ await userService.deleteById(req.params.id);
32
+ ApiResponse.noContent(res);
33
+ });
@@ -0,0 +1,26 @@
1
+ import mongoose, { Schema, InferSchemaType } from "mongoose";
2
+
3
+ const postSchema = new Schema(
4
+ {
5
+ title: {
6
+ type: String,
7
+ required: true,
8
+ trim: true,
9
+ },
10
+ content: {
11
+ type: String,
12
+ required: true,
13
+ },
14
+ published: {
15
+ type: Boolean,
16
+ default: false,
17
+ },
18
+ },
19
+ {
20
+ timestamps: true,
21
+ }
22
+ );
23
+
24
+ export type Post = InferSchemaType<typeof postSchema>;
25
+
26
+ export const PostModel = mongoose.model<Post>("Post", postSchema);
@@ -0,0 +1,25 @@
1
+ import mongoose, { Schema, InferSchemaType } from "mongoose";
2
+
3
+ const userSchema = new Schema(
4
+ {
5
+ email: {
6
+ type: String,
7
+ required: true,
8
+ unique: true,
9
+ lowercase: true,
10
+ trim: true,
11
+ },
12
+ name: {
13
+ type: String,
14
+ default: "Anonymous",
15
+ trim: true,
16
+ },
17
+ },
18
+ {
19
+ timestamps: true,
20
+ }
21
+ );
22
+
23
+ export type User = InferSchemaType<typeof userSchema>;
24
+
25
+ export const UserModel = mongoose.model<User>("User", userSchema);
@@ -0,0 +1,8 @@
1
+ import { Router } from "express";
2
+ import { healthCheck } from "../controllers/health.controller.js";
3
+
4
+ const router = Router();
5
+
6
+ router.get("/", healthCheck);
7
+
8
+ export default router;
@@ -0,0 +1,12 @@
1
+ import { Router } from "express";
2
+ import healthRoutes from "./health.routes.js";
3
+ import userRoutes from "./user.routes.js";
4
+ import postRoutes from "./post.routes.js";
5
+
6
+ const router = Router();
7
+
8
+ router.use("/health", healthRoutes);
9
+ router.use("/users", userRoutes);
10
+ router.use("/posts", postRoutes);
11
+
12
+ export default router;
@@ -0,0 +1,18 @@
1
+ import { Router } from "express";
2
+ import {
3
+ getPosts,
4
+ getPostById,
5
+ createPost,
6
+ updatePost,
7
+ deletePost,
8
+ } from "../controllers/post.controller.js";
9
+
10
+ const router = Router();
11
+
12
+ router.post("/", createPost);
13
+ router.get("/", getPosts);
14
+ router.get("/:id", getPostById);
15
+ router.put("/:id", updatePost);
16
+ router.delete("/:id", deletePost);
17
+
18
+ export default router;
@@ -0,0 +1,18 @@
1
+ import { Router } from "express";
2
+ import {
3
+ createUser,
4
+ getUsers,
5
+ getUserById,
6
+ updateUser,
7
+ deleteUser,
8
+ } from "../controllers/user.controller.js";
9
+
10
+ const router = Router();
11
+
12
+ router.post("/", createUser);
13
+ router.get("/", getUsers);
14
+ router.get("/:id", getUserById);
15
+ router.put("/:id", updateUser);
16
+ router.delete("/:id", deleteUser);
17
+
18
+ export default router;
@@ -0,0 +1,20 @@
1
+ import { logger } from "elseware-nodejs";
2
+
3
+ import app from "./app.js";
4
+ import { env } from "./configs/env.js";
5
+ import { connectDatabase } from "./configs/database.js";
6
+
7
+ const startServer = async (): Promise<void> => {
8
+ logger.configure({
9
+ timestamp: true,
10
+ enabled: env.NODE_ENV !== "production",
11
+ });
12
+
13
+ await connectDatabase();
14
+
15
+ app.listen(env.PORT, () => {
16
+ logger.success(`App running on port ${env.PORT} as ${env.NODE_ENV} mode`);
17
+ });
18
+ };
19
+
20
+ startServer();
@@ -0,0 +1,39 @@
1
+ import type { UpdateQuery } from "mongoose";
2
+ import { BaseService, CrudService, AppError } from "elseware-nodejs";
3
+
4
+ import { UserModel, type User } from "../models/user.model.js";
5
+
6
+ class UserService extends BaseService<User> implements CrudService<User> {
7
+ constructor() {
8
+ super(UserModel, "user");
9
+ }
10
+
11
+ /**
12
+ * Override create with domain-specific logic
13
+ */
14
+ override async create(data: Partial<User>): Promise<User> {
15
+ const existing = await UserModel.findOne({
16
+ email: data.email,
17
+ });
18
+
19
+ if (existing) {
20
+ throw new AppError("User already exists", 400, {
21
+ code: "USER_ALREADY_EXISTS",
22
+ });
23
+ }
24
+
25
+ return super.create(data);
26
+ }
27
+
28
+ /**
29
+ * Optional: override update if needed later
30
+ */
31
+ override async updateById(
32
+ id: string,
33
+ data: UpdateQuery<User>
34
+ ): Promise<User> {
35
+ return super.updateById(id, data);
36
+ }
37
+ }
38
+
39
+ export const userService = new UserService();
@@ -0,0 +1,9 @@
1
+ import request from "supertest";
2
+ import app from "../src/app.js";
3
+
4
+ describe("Health API", () => {
5
+ it("returns ok", async () => {
6
+ const res = await request(app).get("/api/health");
7
+ expect(res.statusCode).toBe(200);
8
+ });
9
+ });
@@ -0,0 +1,13 @@
1
+ import request from "supertest";
2
+ import app from "../src/app.js";
3
+
4
+ describe("User API", () => {
5
+ it("creates a user", async () => {
6
+ const res = await request(app)
7
+ .post("/api/users")
8
+ .send({ email: "test@mail.com" });
9
+
10
+ expect(res.statusCode).toBe(201);
11
+ expect(res.body.email).toBe("test@mail.com");
12
+ });
13
+ });
@@ -0,0 +1,17 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "rootDir": "src",
7
+ "outDir": "dist",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "forceConsistentCasingInFileNames": true,
11
+ "skipLibCheck": true,
12
+ "resolveJsonModule": true,
13
+ "noImplicitAny": false
14
+ },
15
+ "include": ["src"],
16
+ "exclude": ["node_modules", "dist"]
17
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "version": "1.0.0",
3
+ "description": "Sample elseware App for NodeJs",
4
+ "type": "module",
5
+ "main": "server.ts",
6
+ "scripts": {
7
+ "dev": "tsx watch src/server.ts",
8
+ "build": "tsc",
9
+ "start": "node dist/server.js",
10
+ "test": "NODE_ENV=test node --experimental-vm-modules ./node_modules/.bin/jest",
11
+ "coverage": "NODE_ENV=test node --experimental-vm-modules ./node_modules/.bin/jest --coverage",
12
+ "lint": "eslint .",
13
+ "lint:fix": "eslint . --fix"
14
+ },
15
+ "license": "ISC"
16
+ }