express-project-init 0.0.0-stage → 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 +108 -2
- package/dist/features.js +17 -0
- package/dist/features.js.map +1 -0
- package/dist/index.js +46 -0
- package/dist/index.js.map +1 -0
- package/dist/services/file.service.js +73 -0
- package/dist/services/file.service.js.map +1 -0
- package/dist/services/project.service.js +44 -0
- package/dist/services/project.service.js.map +1 -0
- package/dist/services/prompts.service.js +63 -0
- package/dist/services/prompts.service.js.map +1 -0
- package/dist/services/template.service.js +152 -0
- package/dist/services/template.service.js.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +57 -4
- package/templates/javascript/base/package.json +13 -0
- package/templates/javascript/base/src/app.js +22 -0
- package/templates/javascript/base/src/server.js +8 -0
- package/templates/javascript/base/src/utils/helpers.js +3 -0
- package/templates/javascript/base/src/utils/responseHelpers.js +19 -0
- package/templates/javascript/error-handler/src/app.js +28 -0
- package/templates/javascript/error-handler/src/middleware/error.middleware.js +36 -0
- package/templates/javascript/error-handler/src/middleware/notFound.middleware.js +5 -0
- package/templates/javascript/error-handler/src/utils/AppError.js +30 -0
- package/templates/typescript/base/package.json +20 -0
- package/templates/typescript/base/src/app.ts +22 -0
- package/templates/typescript/base/src/server.ts +8 -0
- package/templates/typescript/base/src/utils/helpers.ts +3 -0
- package/templates/typescript/base/src/utils/responseHelpers.ts +42 -0
- package/templates/typescript/base/tsconfig.json +30 -0
- package/templates/typescript/error-handler/src/app.ts +28 -0
- package/templates/typescript/error-handler/src/middleware/error.middleware.ts +42 -0
- package/templates/typescript/error-handler/src/middleware/notFound.middleware.ts +10 -0
- package/templates/typescript/error-handler/src/utils/AppError.ts +44 -0
package/README.md
CHANGED
|
@@ -1,3 +1,109 @@
|
|
|
1
|
-
#
|
|
1
|
+
# ExpressKit
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Scaffold a clean Express API in seconds — JavaScript or TypeScript.**
|
|
4
|
+
|
|
5
|
+
[](docs/demo.mp4)
|
|
6
|
+
|
|
7
|
+
ExpressKit asks four questions, then writes a ready-to-run Express 5 project. Choose JavaScript or TypeScript, optionally add centralized error handling, and start building.
|
|
8
|
+
|
|
9
|
+
## Quick start
|
|
10
|
+
|
|
11
|
+
Requires **Node.js 20+**.
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm create expresskit@latest
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Or run it directly:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npx create-expresskit@latest
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Answer the prompts:
|
|
24
|
+
|
|
25
|
+
```text
|
|
26
|
+
? Project name › my-api
|
|
27
|
+
? Language › TypeScript
|
|
28
|
+
? Create central error handling middleware? › Yes
|
|
29
|
+
? Install dependencies? › Yes
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Then:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
cd my-api
|
|
36
|
+
npm run dev
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Open <http://localhost:3000/health>.
|
|
40
|
+
|
|
41
|
+
Use `.` at the project name prompt to scaffold in the current directory. ExpressKit will not overwrite existing files.
|
|
42
|
+
|
|
43
|
+
## What you get
|
|
44
|
+
|
|
45
|
+
```text
|
|
46
|
+
my-api/
|
|
47
|
+
├── package.json
|
|
48
|
+
├── tsconfig.json # TypeScript only
|
|
49
|
+
└── src/
|
|
50
|
+
├── server.ts
|
|
51
|
+
├── app.ts
|
|
52
|
+
├── middleware/ # with the optional error handler
|
|
53
|
+
│ ├── error.middleware.ts
|
|
54
|
+
│ └── notFound.middleware.ts
|
|
55
|
+
└── utils/
|
|
56
|
+
├── helpers.ts
|
|
57
|
+
├── responseHelpers.ts
|
|
58
|
+
└── AppError.ts # with the optional error handler
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
JavaScript projects use the same layout with `.js` files and no `tsconfig.json`.
|
|
62
|
+
|
|
63
|
+
### Scripts
|
|
64
|
+
|
|
65
|
+
| Command | JavaScript | TypeScript |
|
|
66
|
+
| --- | --- | --- |
|
|
67
|
+
| `npm run dev` | `node --watch` | `tsx watch` |
|
|
68
|
+
| `npm start` | `node` | `tsx` |
|
|
69
|
+
| `npm run typecheck` | — | `tsc --noEmit` |
|
|
70
|
+
|
|
71
|
+
## Error handling
|
|
72
|
+
|
|
73
|
+
With the error handler enabled, throw `BadRequestError`, `UnauthorizedError`, or `NotFoundError` from any route and one middleware formats the response:
|
|
74
|
+
|
|
75
|
+
```json
|
|
76
|
+
{
|
|
77
|
+
"success": false,
|
|
78
|
+
"message": "Route not found",
|
|
79
|
+
"code": "NOT_FOUND"
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Successful responses use the same envelope with `success: true` and `data`.
|
|
84
|
+
|
|
85
|
+
## How it works
|
|
86
|
+
|
|
87
|
+
Projects are assembled from a base template plus optional feature layers. Feature files replace base files at the same path, and feature `package.json` fragments are merged into the base manifest.
|
|
88
|
+
|
|
89
|
+
## Local development
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
npm install
|
|
93
|
+
npm run build
|
|
94
|
+
npm link
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Then, in a separate directory:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
mkdir -p /tmp/expresskit-test
|
|
101
|
+
cd /tmp/expresskit-test
|
|
102
|
+
create-expresskit
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Rebuild with `npm run build` after changing `src/`; the linked command picks up the new `dist/` automatically. Remove the global link with `npm unlink -g create-expresskit`.
|
|
106
|
+
|
|
107
|
+
## License
|
|
108
|
+
|
|
109
|
+
MIT
|
package/dist/features.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every optional feature lives here once.
|
|
3
|
+
*
|
|
4
|
+
* To add a feature:
|
|
5
|
+
* 1. Add templates/<language>/<id>/ with the files it adds or overrides.
|
|
6
|
+
* 2. Add an entry to this array.
|
|
7
|
+
*
|
|
8
|
+
* The prompt, file layering, and package.json merging all derive from this list.
|
|
9
|
+
*/
|
|
10
|
+
export const FEATURES = [
|
|
11
|
+
{
|
|
12
|
+
id: "error-handler",
|
|
13
|
+
prompt: "Create central error handling middleware?",
|
|
14
|
+
defaultValue: true,
|
|
15
|
+
},
|
|
16
|
+
];
|
|
17
|
+
//# sourceMappingURL=features.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"features.js","sourceRoot":"","sources":["../src/features.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG;IACtB;QACE,EAAE,EAAE,eAAe;QACnB,MAAM,EAAE,2CAA2C;QACnD,YAAY,EAAE,IAAI;KACnB;CACO,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spinner } from "@clack/prompts";
|
|
3
|
+
import { FileService } from "./services/file.service.js";
|
|
4
|
+
import { ProjectService } from "./services/project.service.js";
|
|
5
|
+
import { PromptService } from "./services/prompts.service.js";
|
|
6
|
+
import { TemplateService } from "./services/template.service.js";
|
|
7
|
+
async function main() {
|
|
8
|
+
const promptService = new PromptService();
|
|
9
|
+
const fileService = new FileService();
|
|
10
|
+
const templateService = new TemplateService();
|
|
11
|
+
// 1. Collect every answer first — the spinner only starts after all prompts are done
|
|
12
|
+
const projectName = await promptService.askProjectName();
|
|
13
|
+
const language = await promptService.askLanguage();
|
|
14
|
+
const availableFeatures = await templateService.getAvailableFeatures(language);
|
|
15
|
+
const features = await promptService.askFeatureAnswers(availableFeatures);
|
|
16
|
+
const installDependencies = await promptService.askShouldInstall();
|
|
17
|
+
const answers = {
|
|
18
|
+
projectName,
|
|
19
|
+
language,
|
|
20
|
+
features,
|
|
21
|
+
installDependencies,
|
|
22
|
+
};
|
|
23
|
+
// 2. Run the commands behind a spinner
|
|
24
|
+
const loading = spinner();
|
|
25
|
+
loading.start("Creating project");
|
|
26
|
+
try {
|
|
27
|
+
const projectPath = await fileService.createDirectory(projectName);
|
|
28
|
+
const projectService = new ProjectService(projectPath, answers);
|
|
29
|
+
await projectService.createProject();
|
|
30
|
+
if (installDependencies) {
|
|
31
|
+
loading.message("Installing dependencies");
|
|
32
|
+
await projectService.runInstallCommand();
|
|
33
|
+
}
|
|
34
|
+
loading.stop(`Created ${language} project at ${projectPath}`);
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
loading.error("Project creation failed");
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
main().catch((err) => {
|
|
42
|
+
const message = err instanceof Error ? err.message : "An unexpected error occurred";
|
|
43
|
+
console.error(`ExpressKit: ${message}`);
|
|
44
|
+
process.exitCode = 1;
|
|
45
|
+
});
|
|
46
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAEzC,OAAO,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AACzD,OAAO,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAC/D,OAAO,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAC9D,OAAO,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAC;AAEjE,KAAK,UAAU,IAAI;IACjB,MAAM,aAAa,GAAG,IAAI,aAAa,EAAE,CAAC;IAC1C,MAAM,WAAW,GAAG,IAAI,WAAW,EAAE,CAAC;IACtC,MAAM,eAAe,GAAG,IAAI,eAAe,EAAE,CAAC;IAE9C,qFAAqF;IACrF,MAAM,WAAW,GAAG,MAAM,aAAa,CAAC,cAAc,EAAE,CAAC;IACzD,MAAM,QAAQ,GAAG,MAAM,aAAa,CAAC,WAAW,EAAE,CAAC;IACnD,MAAM,iBAAiB,GACrB,MAAM,eAAe,CAAC,oBAAoB,CAAC,QAAQ,CAAC,CAAC;IACvD,MAAM,QAAQ,GAAG,MAAM,aAAa,CAAC,iBAAiB,CAAC,iBAAiB,CAAC,CAAC;IAC1E,MAAM,mBAAmB,GAAG,MAAM,aAAa,CAAC,gBAAgB,EAAE,CAAC;IAEnE,MAAM,OAAO,GAAmB;QAC9B,WAAW;QACX,QAAQ;QACR,QAAQ;QACR,mBAAmB;KACpB,CAAC;IAEF,uCAAuC;IACvC,MAAM,OAAO,GAAG,OAAO,EAAE,CAAC;IAC1B,OAAO,CAAC,KAAK,CAAC,kBAAkB,CAAC,CAAC;IAElC,IAAI,CAAC;QACH,MAAM,WAAW,GAAG,MAAM,WAAW,CAAC,eAAe,CAAC,WAAW,CAAC,CAAC;QACnE,MAAM,cAAc,GAAG,IAAI,cAAc,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;QAChE,MAAM,cAAc,CAAC,aAAa,EAAE,CAAC;QAErC,IAAI,mBAAmB,EAAE,CAAC;YACxB,OAAO,CAAC,OAAO,CAAC,yBAAyB,CAAC,CAAC;YAC3C,MAAM,cAAc,CAAC,iBAAiB,EAAE,CAAC;QAC3C,CAAC;QAED,OAAO,CAAC,IAAI,CAAC,WAAW,QAAQ,eAAe,WAAW,EAAE,CAAC,CAAC;IAChE,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,KAAK,CAAC,yBAAyB,CAAC,CAAC;QACzC,MAAM,KAAK,CAAC;IACd,CAAC;AACH,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;IAC5B,MAAM,OAAO,GACX,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,8BAA8B,CAAC;IAEtE,OAAO,CAAC,KAAK,CAAC,eAAe,OAAO,EAAE,CAAC,CAAC;IACxC,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;AACvB,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { access, mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
export class FileService {
|
|
4
|
+
async createDirectory(projectName) {
|
|
5
|
+
const dirPath = path.resolve(process.cwd(), projectName);
|
|
6
|
+
try {
|
|
7
|
+
await mkdir(dirPath, { recursive: true });
|
|
8
|
+
return dirPath;
|
|
9
|
+
}
|
|
10
|
+
catch (error) {
|
|
11
|
+
const message = error instanceof Error ? error.message : "Unknown filesystem error";
|
|
12
|
+
throw new Error(`Failed to create project directory "${dirPath}": ${message}`, { cause: error });
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Writes the complete, already-composed project in one pass.
|
|
17
|
+
* Every destination is checked before the first file is written.
|
|
18
|
+
*/
|
|
19
|
+
async writeFiles(projectDirectory, files) {
|
|
20
|
+
const entries = Object.entries(files);
|
|
21
|
+
if (entries.length === 0) {
|
|
22
|
+
throw new Error("No files to create.");
|
|
23
|
+
}
|
|
24
|
+
const destinations = entries.map(([relativePath, contents]) => ({
|
|
25
|
+
relativePath,
|
|
26
|
+
contents,
|
|
27
|
+
absolutePath: this.resolveProjectPath(projectDirectory, relativePath),
|
|
28
|
+
}));
|
|
29
|
+
const existingFiles = await this.getExistingFiles(destinations);
|
|
30
|
+
if (existingFiles.length > 0) {
|
|
31
|
+
throw new Error(`Cannot create project: these files already exist in "${projectDirectory}": ${existingFiles.join(", ")}`);
|
|
32
|
+
}
|
|
33
|
+
const results = await Promise.allSettled(destinations.map(({ absolutePath, contents }) => this.writeFile(absolutePath, contents)));
|
|
34
|
+
const failures = results.filter((result) => result.status === "rejected");
|
|
35
|
+
if (failures.length > 0) {
|
|
36
|
+
throw new Error(`Failed to create ${failures.length} of ${entries.length} files.`, { cause: failures.map((failure) => failure.reason) });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
resolveProjectPath(projectDirectory, relativePath) {
|
|
40
|
+
if (!relativePath.trim() || path.isAbsolute(relativePath)) {
|
|
41
|
+
throw new Error(`Generated file path must be a non-empty relative path: "${relativePath}".`);
|
|
42
|
+
}
|
|
43
|
+
const projectRoot = path.resolve(projectDirectory);
|
|
44
|
+
const destination = path.resolve(projectRoot, relativePath);
|
|
45
|
+
if (destination !== projectRoot &&
|
|
46
|
+
!destination.startsWith(`${projectRoot}${path.sep}`)) {
|
|
47
|
+
throw new Error(`Generated file path escapes the project directory: "${relativePath}".`);
|
|
48
|
+
}
|
|
49
|
+
return destination;
|
|
50
|
+
}
|
|
51
|
+
async getExistingFiles(files) {
|
|
52
|
+
const checks = files.map(async ({ relativePath, absolutePath }) => {
|
|
53
|
+
try {
|
|
54
|
+
await access(absolutePath);
|
|
55
|
+
return relativePath;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
const results = await Promise.all(checks);
|
|
62
|
+
return results.filter((file) => file !== null);
|
|
63
|
+
}
|
|
64
|
+
async writeFile(filePath, contents) {
|
|
65
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
66
|
+
// "wx" fails if the file exists, so we never overwrite user files.
|
|
67
|
+
await writeFile(filePath, contents, {
|
|
68
|
+
encoding: "utf-8",
|
|
69
|
+
flag: "wx",
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=file.service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file.service.js","sourceRoot":"","sources":["../../src/services/file.service.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { FileService } from "./file.service.js";
|
|
3
|
+
import { TemplateService } from "./template.service.js";
|
|
4
|
+
export class ProjectService {
|
|
5
|
+
projectPath;
|
|
6
|
+
answers;
|
|
7
|
+
fileService;
|
|
8
|
+
templateService;
|
|
9
|
+
constructor(projectPath, answers, fileService = new FileService(), templateService = new TemplateService()) {
|
|
10
|
+
this.projectPath = projectPath;
|
|
11
|
+
this.answers = answers;
|
|
12
|
+
this.fileService = fileService;
|
|
13
|
+
this.templateService = templateService;
|
|
14
|
+
}
|
|
15
|
+
async createProject() {
|
|
16
|
+
const files = await this.templateService.buildFilePlan(this.answers);
|
|
17
|
+
// Build the whole plan first, then write it in one pass.
|
|
18
|
+
await this.fileService.writeFiles(this.projectPath, files);
|
|
19
|
+
}
|
|
20
|
+
async runInstallCommand() {
|
|
21
|
+
return new Promise((resolve, reject) => {
|
|
22
|
+
const child = spawn("npm", ["install"], {
|
|
23
|
+
cwd: this.projectPath, // run inside the NEW project, not where the CLI was launched
|
|
24
|
+
stdio: ["ignore", "ignore", "pipe"], // hide npm output from the spinner; stderr kept for errors
|
|
25
|
+
shell: process.platform === "win32",
|
|
26
|
+
});
|
|
27
|
+
let stderr = "";
|
|
28
|
+
child.stderr?.on("data", (chunk) => {
|
|
29
|
+
stderr += chunk.toString();
|
|
30
|
+
});
|
|
31
|
+
child.on("error", reject);
|
|
32
|
+
child.on("close", (code) => {
|
|
33
|
+
if (code === 0) {
|
|
34
|
+
resolve();
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
const details = stderr.trim();
|
|
38
|
+
reject(new Error(`npm install failed with exit code ${code}${details ? `\n${details}` : ""}`));
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=project.service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"project.service.js","sourceRoot":"","sources":["../../src/services/project.service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAE3C,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAExD,MAAM,OAAO,cAAc;IACR,WAAW,CAAS;IACpB,OAAO,CAAiB;IACxB,WAAW,CAAc;IACzB,eAAe,CAAkB;IAElD,YACE,WAAmB,EACnB,OAAuB,EACvB,WAAW,GAAG,IAAI,WAAW,EAAE,EAC/B,eAAe,GAAG,IAAI,eAAe,EAAE;QAEvC,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC;QAC/B,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC;QAC/B,IAAI,CAAC,eAAe,GAAG,eAAe,CAAC;IACzC,CAAC;IAEM,KAAK,CAAC,aAAa;QACxB,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,eAAe,CAAC,aAAa,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAErE,yDAAyD;QACzD,MAAM,IAAI,CAAC,WAAW,CAAC,UAAU,CAAC,IAAI,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC;IAC7D,CAAC;IAEM,KAAK,CAAC,iBAAiB;QAC5B,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YACrC,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,EAAE,CAAC,SAAS,CAAC,EAAE;gBACtC,GAAG,EAAE,IAAI,CAAC,WAAW,EAAE,6DAA6D;gBACpF,KAAK,EAAE,CAAC,QAAQ,EAAE,QAAQ,EAAE,MAAM,CAAC,EAAE,2DAA2D;gBAChG,KAAK,EAAE,OAAO,CAAC,QAAQ,KAAK,OAAO;aACpC,CAAC,CAAC;YAEH,IAAI,MAAM,GAAG,EAAE,CAAC;YAChB,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,CAAC,KAAa,EAAE,EAAE;gBACzC,MAAM,IAAI,KAAK,CAAC,QAAQ,EAAE,CAAC;YAC7B,CAAC,CAAC,CAAC;YAEH,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;YAE1B,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,IAAY,EAAE,EAAE;gBACjC,IAAI,IAAI,KAAK,CAAC,EAAE,CAAC;oBACf,OAAO,EAAE,CAAC;gBACZ,CAAC;qBAAM,CAAC;oBACN,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC;oBAE9B,MAAM,CACJ,IAAI,KAAK,CACP,qCAAqC,IAAI,GAAG,OAAO,CAAC,CAAC,CAAC,KAAK,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAC5E,CACF,CAAC;gBACJ,CAAC;YACH,CAAC,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;IACL,CAAC;CACF"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { cancel, confirm, isCancel, select, text } from "@clack/prompts";
|
|
2
|
+
import { FEATURES } from "../features.js";
|
|
3
|
+
export class PromptService {
|
|
4
|
+
LANGUAGE_OPTIONS = [
|
|
5
|
+
{ value: "javascript", label: "JavaScript" },
|
|
6
|
+
{ value: "typescript", label: "TypeScript" },
|
|
7
|
+
];
|
|
8
|
+
async askProjectName() {
|
|
9
|
+
const response = await text({
|
|
10
|
+
message: "What would you like to name your project?",
|
|
11
|
+
placeholder: "my-api",
|
|
12
|
+
defaultValue: ".",
|
|
13
|
+
validate(value) {
|
|
14
|
+
if (!value?.trim()) {
|
|
15
|
+
return "Please enter a project name or '.' to use the current directory.";
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
this.handleCancel(response);
|
|
20
|
+
return response.trim();
|
|
21
|
+
}
|
|
22
|
+
async askLanguage() {
|
|
23
|
+
const response = await select({
|
|
24
|
+
message: "Which language would you like to use?",
|
|
25
|
+
options: this.LANGUAGE_OPTIONS,
|
|
26
|
+
initialValue: "javascript",
|
|
27
|
+
});
|
|
28
|
+
this.handleCancel(response);
|
|
29
|
+
return response;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Asks one confirm per registered feature available for the chosen
|
|
33
|
+
* language. Unavailable features default to false.
|
|
34
|
+
*/
|
|
35
|
+
async askFeatureAnswers(availableFeatures) {
|
|
36
|
+
const answers = Object.fromEntries(FEATURES.map((feature) => [feature.id, false]));
|
|
37
|
+
for (const feature of availableFeatures) {
|
|
38
|
+
const response = await confirm({
|
|
39
|
+
message: feature.prompt,
|
|
40
|
+
initialValue: feature.defaultValue,
|
|
41
|
+
});
|
|
42
|
+
this.handleCancel(response);
|
|
43
|
+
answers[feature.id] = response;
|
|
44
|
+
}
|
|
45
|
+
return answers;
|
|
46
|
+
}
|
|
47
|
+
async askShouldInstall() {
|
|
48
|
+
const response = await confirm({
|
|
49
|
+
message: "Install dependencies?",
|
|
50
|
+
initialValue: true,
|
|
51
|
+
});
|
|
52
|
+
this.handleCancel(response);
|
|
53
|
+
return response;
|
|
54
|
+
}
|
|
55
|
+
// Catches Ctrl+C / Esc so the CLI exits cleanly instead of crashing.
|
|
56
|
+
handleCancel(response) {
|
|
57
|
+
if (isCancel(response)) {
|
|
58
|
+
cancel("Project setup cancelled.");
|
|
59
|
+
process.exit(0);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=prompts.service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prompts.service.js","sourceRoot":"","sources":["../../src/services/prompts.service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AACzE,OAAO,EAAE,QAAQ,EAAgB,MAAM,gBAAgB,CAAC;AAGxD,MAAM,OAAO,aAAa;IACP,gBAAgB,GAG3B;QACJ,EAAE,KAAK,EAAE,YAAY,EAAE,KAAK,EAAE,YAAY,EAAE;QAC5C,EAAE,KAAK,EAAE,YAAY,EAAE,KAAK,EAAE,YAAY,EAAE;KAC7C,CAAC;IAEK,KAAK,CAAC,cAAc;QACzB,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC;YAC1B,OAAO,EAAE,2CAA2C;YACpD,WAAW,EAAE,QAAQ;YACrB,YAAY,EAAE,GAAG;YACjB,QAAQ,CAAC,KAAK;gBACZ,IAAI,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,CAAC;oBACnB,OAAO,kEAAkE,CAAC;gBAC5E,CAAC;YACH,CAAC;SACF,CAAC,CAAC;QAEH,IAAI,CAAC,YAAY,CAAS,QAAQ,CAAC,CAAC;QAEpC,OAAO,QAAQ,CAAC,IAAI,EAAE,CAAC;IACzB,CAAC;IAEM,KAAK,CAAC,WAAW;QACtB,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC;YAC5B,OAAO,EAAE,uCAAuC;YAChD,OAAO,EAAE,IAAI,CAAC,gBAAgB;YAC9B,YAAY,EAAE,YAAY;SAC3B,CAAC,CAAC;QAEH,IAAI,CAAC,YAAY,CAAW,QAAQ,CAAC,CAAC;QAEtC,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED;;;OAGG;IACI,KAAK,CAAC,iBAAiB,CAC5B,iBAAqC;QAErC,MAAM,OAAO,GAAG,MAAM,CAAC,WAAW,CAChC,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC,CAC7B,CAAC;QAEpB,KAAK,MAAM,OAAO,IAAI,iBAAiB,EAAE,CAAC;YACxC,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC;gBAC7B,OAAO,EAAE,OAAO,CAAC,MAAM;gBACvB,YAAY,EAAE,OAAO,CAAC,YAAY;aACnC,CAAC,CAAC;YAEH,IAAI,CAAC,YAAY,CAAU,QAAQ,CAAC,CAAC;YAErC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,GAAG,QAAQ,CAAC;QACjC,CAAC;QAED,OAAO,OAAO,CAAC;IACjB,CAAC;IAEM,KAAK,CAAC,gBAAgB;QAC3B,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC;YAC7B,OAAO,EAAE,uBAAuB;YAChC,YAAY,EAAE,IAAI;SACnB,CAAC,CAAC;QAEH,IAAI,CAAC,YAAY,CAAU,QAAQ,CAAC,CAAC;QAErC,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED,qEAAqE;IAC7D,YAAY,CAAI,QAAiB;QACvC,IAAI,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;YACvB,MAAM,CAAC,0BAA0B,CAAC,CAAC;YACnC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;IACH,CAAC;CACF"}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { access, readdir, readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { FEATURES } from "../features.js";
|
|
5
|
+
const PACKAGE_SECTIONS = [
|
|
6
|
+
"dependencies",
|
|
7
|
+
"devDependencies",
|
|
8
|
+
"scripts",
|
|
9
|
+
];
|
|
10
|
+
export class TemplateService {
|
|
11
|
+
templateRoot;
|
|
12
|
+
constructor(templateRoot) {
|
|
13
|
+
if (templateRoot) {
|
|
14
|
+
this.templateRoot = path.resolve(templateRoot);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const serviceDirectory = path.dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
// templates/ sits two levels up from src/services (and dist/services).
|
|
19
|
+
this.templateRoot = path.resolve(serviceDirectory, "../../templates");
|
|
20
|
+
}
|
|
21
|
+
async getAvailableFeatures(language) {
|
|
22
|
+
const languageDirectory = path.join(this.templateRoot, language);
|
|
23
|
+
let entries;
|
|
24
|
+
try {
|
|
25
|
+
entries = await readdir(languageDirectory, { withFileTypes: true });
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
throw new Error(`Unable to read templates for "${language}" at "${languageDirectory}".`, { cause: error });
|
|
29
|
+
}
|
|
30
|
+
const folders = new Set(entries
|
|
31
|
+
.filter((entry) => entry.isDirectory())
|
|
32
|
+
.map((entry) => entry.name));
|
|
33
|
+
return FEATURES.filter((feature) => folders.has(feature.id));
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Builds the complete project in memory.
|
|
37
|
+
*
|
|
38
|
+
* Base files come first. Each enabled feature is then layered on top:
|
|
39
|
+
* feature files replace base files with the same relative path, while new
|
|
40
|
+
* paths are added. package.json is the exception — feature package.json
|
|
41
|
+
* files are fragments merged into the base package.json.
|
|
42
|
+
*/
|
|
43
|
+
async buildFilePlan(answers) {
|
|
44
|
+
const baseFiles = await this.readLayer(answers.language, "base");
|
|
45
|
+
const basePackageSource = baseFiles["package.json"];
|
|
46
|
+
if (basePackageSource === undefined) {
|
|
47
|
+
throw new Error(`Base template for "${answers.language}" is missing package.json.`);
|
|
48
|
+
}
|
|
49
|
+
let packageJson = this.parseJson(basePackageSource, `${answers.language}/base/package.json`);
|
|
50
|
+
let files = { ...baseFiles };
|
|
51
|
+
for (const feature of FEATURES) {
|
|
52
|
+
if (!answers.features[feature.id]) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const layer = await this.readLayer(answers.language, feature.id);
|
|
56
|
+
const fragmentSource = layer["package.json"];
|
|
57
|
+
if (fragmentSource !== undefined) {
|
|
58
|
+
const fragment = this.parsePackageFragment(fragmentSource, feature.id);
|
|
59
|
+
packageJson = this.mergePackageJson(packageJson, fragment, feature.id);
|
|
60
|
+
// The merged package.json is written last, not copied from the layer.
|
|
61
|
+
delete layer["package.json"];
|
|
62
|
+
}
|
|
63
|
+
files = { ...files, ...layer };
|
|
64
|
+
}
|
|
65
|
+
files["package.json"] = `${JSON.stringify(packageJson, null, 2)}\n`;
|
|
66
|
+
return files;
|
|
67
|
+
}
|
|
68
|
+
async readLayer(language, layerName) {
|
|
69
|
+
const layerDirectory = path.join(this.templateRoot, language, layerName);
|
|
70
|
+
try {
|
|
71
|
+
await access(layerDirectory);
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
throw new Error(`Template folder not found: "${language}/${layerName}".`, { cause: error });
|
|
75
|
+
}
|
|
76
|
+
return this.readDirectory(layerDirectory);
|
|
77
|
+
}
|
|
78
|
+
async readDirectory(root, relativeDirectory = "") {
|
|
79
|
+
const directory = path.join(root, relativeDirectory);
|
|
80
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
81
|
+
const files = {};
|
|
82
|
+
for (const entry of entries) {
|
|
83
|
+
const relativePath = relativeDirectory
|
|
84
|
+
? path.posix.join(relativeDirectory, entry.name)
|
|
85
|
+
: entry.name;
|
|
86
|
+
if (entry.isDirectory()) {
|
|
87
|
+
Object.assign(files, await this.readDirectory(root, relativePath));
|
|
88
|
+
}
|
|
89
|
+
else if (entry.isFile()) {
|
|
90
|
+
files[relativePath] = await readFile(path.join(root, relativePath), "utf8");
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return files;
|
|
94
|
+
}
|
|
95
|
+
parsePackageFragment(source, featureId) {
|
|
96
|
+
const parsed = this.parseJson(source, `${featureId}/package.json`);
|
|
97
|
+
const unknownKeys = Object.keys(parsed).filter((key) => !PACKAGE_SECTIONS.includes(key));
|
|
98
|
+
if (unknownKeys.length > 0) {
|
|
99
|
+
throw new Error(`Feature "${featureId}" has unsupported package.json keys: ${unknownKeys.join(", ")}. Only ${PACKAGE_SECTIONS.join(", ")} can be merged.`);
|
|
100
|
+
}
|
|
101
|
+
const fragment = {};
|
|
102
|
+
for (const section of PACKAGE_SECTIONS) {
|
|
103
|
+
const value = parsed[section];
|
|
104
|
+
if (value === undefined) {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (!this.isStringRecord(value)) {
|
|
108
|
+
throw new Error(`"${section}" in feature "${featureId}" package.json must be an object of string values.`);
|
|
109
|
+
}
|
|
110
|
+
fragment[section] = value;
|
|
111
|
+
}
|
|
112
|
+
return fragment;
|
|
113
|
+
}
|
|
114
|
+
mergePackageJson(base, fragment, featureId) {
|
|
115
|
+
const merged = { ...base };
|
|
116
|
+
for (const section of PACKAGE_SECTIONS) {
|
|
117
|
+
const additions = fragment[section];
|
|
118
|
+
if (!additions) {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
const target = merged[section]
|
|
122
|
+
? { ...merged[section] }
|
|
123
|
+
: {};
|
|
124
|
+
for (const [name, value] of Object.entries(additions)) {
|
|
125
|
+
const existing = target[name];
|
|
126
|
+
if (existing !== undefined && existing !== value) {
|
|
127
|
+
throw new Error(`Feature "${featureId}" conflicts with package.json ${section} entry "${name}": "${existing}" (existing) vs "${value}" (feature).`);
|
|
128
|
+
}
|
|
129
|
+
target[name] = value;
|
|
130
|
+
}
|
|
131
|
+
merged[section] = target;
|
|
132
|
+
}
|
|
133
|
+
return merged;
|
|
134
|
+
}
|
|
135
|
+
isStringRecord(value) {
|
|
136
|
+
return (typeof value === "object" &&
|
|
137
|
+
value !== null &&
|
|
138
|
+
!Array.isArray(value) &&
|
|
139
|
+
Object.values(value).every((item) => typeof item === "string"));
|
|
140
|
+
}
|
|
141
|
+
parseJson(source, templatePath) {
|
|
142
|
+
try {
|
|
143
|
+
return JSON.parse(source);
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
throw new Error(`Invalid JSON in template "${templatePath}".`, {
|
|
147
|
+
cause: error,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
//# sourceMappingURL=template.service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"template.service.js","sourceRoot":"","sources":["../../src/services/template.service.ts"],"names":[],"mappings":"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"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
package/package.json
CHANGED
|
@@ -1,6 +1,59 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "express-project-init",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Create production-ready Express.js API projects with JavaScript or TypeScript using clean, reusable starter templates.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"express-project-init": "./dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"templates",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE"
|
|
15
|
+
],
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=20"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"dev": "tsx src/index.ts",
|
|
21
|
+
"clean": "node -e \"const fs=require('node:fs'); for (const p of ['dist','tsconfig.tsbuildinfo']) fs.rmSync(p,{recursive:true,force:true})\"",
|
|
22
|
+
"build": "tsc",
|
|
23
|
+
"typecheck": "tsc --noEmit",
|
|
24
|
+
"prepack": "npm run clean && npm run build",
|
|
25
|
+
"start": "node dist/index.js"
|
|
26
|
+
},
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/raunak-ray/ExpressKit.git"
|
|
30
|
+
},
|
|
31
|
+
"homepage": "https://github.com/raunak-ray/ExpressKit#readme",
|
|
32
|
+
"bugs": {
|
|
33
|
+
"url": "https://github.com/raunak-ray/ExpressKit/issues"
|
|
34
|
+
},
|
|
35
|
+
"keywords": [
|
|
36
|
+
"express",
|
|
37
|
+
"expressjs",
|
|
38
|
+
"express-generator",
|
|
39
|
+
"express-cli",
|
|
40
|
+
"nodejs",
|
|
41
|
+
"api-generator",
|
|
42
|
+
"backend",
|
|
43
|
+
"project-scaffolding",
|
|
44
|
+
"boilerplate",
|
|
45
|
+
"starter-template",
|
|
46
|
+
"typescript",
|
|
47
|
+
"javascript"
|
|
48
|
+
],
|
|
49
|
+
"author": "Raunak Ray",
|
|
50
|
+
"license": "MIT",
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"@clack/prompts": "^1.8.1"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@types/node": "^26.6.5",
|
|
56
|
+
"tsx": "^4.23.15",
|
|
57
|
+
"typescript": "^7.0.2"
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "my-api",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Express API generated by ExpressKit",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "node --watch src/server.js",
|
|
8
|
+
"start": "node src/server.js"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"express": "^5.1.0"
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import express from "express";
|
|
2
|
+
import { getTimeStamp } from "./utils/helpers.js";
|
|
3
|
+
import { successResponse } from "./utils/responseHelpers.js";
|
|
4
|
+
|
|
5
|
+
const app = express();
|
|
6
|
+
|
|
7
|
+
app.use(express.json());
|
|
8
|
+
|
|
9
|
+
// Health check
|
|
10
|
+
app.get("/health", (_req, res) =>
|
|
11
|
+
successResponse(res, {
|
|
12
|
+
statusCode: 200,
|
|
13
|
+
message: "API is up and running",
|
|
14
|
+
data: {
|
|
15
|
+
status: "ok",
|
|
16
|
+
uptime: Math.round(process.uptime()),
|
|
17
|
+
timestamp: getTimeStamp(),
|
|
18
|
+
},
|
|
19
|
+
}),
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
export default app;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export function successResponse(res, { statusCode = 200, message, data }) {
|
|
2
|
+
return res.status(statusCode).json({
|
|
3
|
+
success: true,
|
|
4
|
+
message,
|
|
5
|
+
data,
|
|
6
|
+
});
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function errorResponse(
|
|
10
|
+
res,
|
|
11
|
+
{ statusCode = 500, message, code = "INTERNAL_ERROR", fields },
|
|
12
|
+
) {
|
|
13
|
+
return res.status(statusCode).json({
|
|
14
|
+
success: false,
|
|
15
|
+
message,
|
|
16
|
+
code,
|
|
17
|
+
fields,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import express from "express";
|
|
2
|
+
import errorMiddleware from "./middleware/error.middleware.js";
|
|
3
|
+
import notFoundMiddleware from "./middleware/notFound.middleware.js";
|
|
4
|
+
import { getTimeStamp } from "./utils/helpers.js";
|
|
5
|
+
import { successResponse } from "./utils/responseHelpers.js";
|
|
6
|
+
|
|
7
|
+
const app = express();
|
|
8
|
+
|
|
9
|
+
app.use(express.json());
|
|
10
|
+
|
|
11
|
+
// Health check
|
|
12
|
+
app.get("/health", (_req, res) =>
|
|
13
|
+
successResponse(res, {
|
|
14
|
+
statusCode: 200,
|
|
15
|
+
message: "API is up and running",
|
|
16
|
+
data: {
|
|
17
|
+
status: "ok",
|
|
18
|
+
uptime: Math.round(process.uptime()),
|
|
19
|
+
timestamp: getTimeStamp(),
|
|
20
|
+
},
|
|
21
|
+
}),
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
// Unknown routes → 404, then every error gets formatted in one place.
|
|
25
|
+
app.use(notFoundMiddleware);
|
|
26
|
+
app.use(errorMiddleware);
|
|
27
|
+
|
|
28
|
+
export default app;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { AppError, BadRequestError } from "../utils/AppError.js";
|
|
2
|
+
import { errorResponse } from "../utils/responseHelpers.js";
|
|
3
|
+
import { getTimeStamp } from "../utils/helpers.js";
|
|
4
|
+
|
|
5
|
+
// Express error handler — needs all 4 params, that's how Express recognizes it.
|
|
6
|
+
export default function errorMiddleware(error, req, res, next) {
|
|
7
|
+
const timeStamp = getTimeStamp();
|
|
8
|
+
|
|
9
|
+
// Expected errors — log them and send their details
|
|
10
|
+
if (error instanceof AppError) {
|
|
11
|
+
console.log(
|
|
12
|
+
`[${timeStamp}] [ERROR] ${req.method} ${req.originalUrl} - ${error.message}`,
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
return errorResponse(res, {
|
|
16
|
+
statusCode: error.statusCode,
|
|
17
|
+
message: error.message,
|
|
18
|
+
code: error.code,
|
|
19
|
+
fields: error instanceof BadRequestError ? error.fields : undefined, // fields only for bad requests
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Not an AppError — build a message if it's an Error, otherwise fall back
|
|
24
|
+
const message =
|
|
25
|
+
error instanceof Error ? error.message : "Internal server error";
|
|
26
|
+
|
|
27
|
+
console.error(
|
|
28
|
+
`[${timeStamp}] [ERROR] ${req.method} ${req.originalUrl} - ${message}`,
|
|
29
|
+
error,
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
return errorResponse(res, {
|
|
33
|
+
statusCode: 500,
|
|
34
|
+
message,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export class AppError extends Error {
|
|
2
|
+
constructor(message, { code = "INTERNAL_ERROR", statusCode = 500 } = {}) {
|
|
3
|
+
super(message);
|
|
4
|
+
|
|
5
|
+
this.name = this.constructor.name;
|
|
6
|
+
this.code = code; // machine-readable code, e.g. "BAD_REQUEST"
|
|
7
|
+
this.statusCode = statusCode; // HTTP status to send back
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class BadRequestError extends AppError {
|
|
12
|
+
// fields: validation errors, e.g. [{ field: "email", message: "required" }]
|
|
13
|
+
constructor(message = "Bad request", fields = []) {
|
|
14
|
+
super(message, { code: "BAD_REQUEST", statusCode: 400 });
|
|
15
|
+
|
|
16
|
+
this.fields = fields;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class UnauthorizedError extends AppError {
|
|
21
|
+
constructor(message = "Unauthorized") {
|
|
22
|
+
super(message, { code: "UNAUTHORIZED", statusCode: 401 });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class NotFoundError extends AppError {
|
|
27
|
+
constructor(message = "Not found") {
|
|
28
|
+
super(message, { code: "NOT_FOUND", statusCode: 404 });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "my-api",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Express API generated by ExpressKit",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "tsx watch src/server.ts",
|
|
8
|
+
"start": "tsx src/server.ts",
|
|
9
|
+
"typecheck": "tsc --noEmit"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"express": "^5.1.0"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"@types/express": "^5.0.0",
|
|
16
|
+
"@types/node": "^26.6.5",
|
|
17
|
+
"tsx": "^4.23.15",
|
|
18
|
+
"typescript": "^5.9.0"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import express, { type Request, type Response } from "express";
|
|
2
|
+
import { getTimeStamp } from "./utils/helpers";
|
|
3
|
+
import { successResponse } from "./utils/responseHelpers";
|
|
4
|
+
|
|
5
|
+
const app = express();
|
|
6
|
+
|
|
7
|
+
app.use(express.json());
|
|
8
|
+
|
|
9
|
+
// Health check
|
|
10
|
+
app.get("/health", (_req: Request, res: Response) =>
|
|
11
|
+
successResponse(res, {
|
|
12
|
+
statusCode: 200,
|
|
13
|
+
message: "API is up and running",
|
|
14
|
+
data: {
|
|
15
|
+
status: "ok",
|
|
16
|
+
uptime: Math.round(process.uptime()),
|
|
17
|
+
timestamp: getTimeStamp(),
|
|
18
|
+
},
|
|
19
|
+
}),
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
export default app;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Response } from "express";
|
|
2
|
+
|
|
3
|
+
export type FieldError = {
|
|
4
|
+
field: string;
|
|
5
|
+
message: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
type SuccessOptions<T> = {
|
|
9
|
+
statusCode?: number;
|
|
10
|
+
message: string;
|
|
11
|
+
data?: T;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function successResponse<T>(
|
|
15
|
+
res: Response,
|
|
16
|
+
{ statusCode = 200, message, data }: SuccessOptions<T>,
|
|
17
|
+
): Response {
|
|
18
|
+
return res.status(statusCode).json({
|
|
19
|
+
success: true,
|
|
20
|
+
message,
|
|
21
|
+
data,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type ErrorOptions = {
|
|
26
|
+
statusCode?: number;
|
|
27
|
+
message: string;
|
|
28
|
+
code?: string;
|
|
29
|
+
fields?: FieldError[];
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export function errorResponse(
|
|
33
|
+
res: Response,
|
|
34
|
+
{ statusCode = 500, message, code = "INTERNAL_ERROR", fields }: ErrorOptions,
|
|
35
|
+
): Response {
|
|
36
|
+
return res.status(statusCode).json({
|
|
37
|
+
success: false,
|
|
38
|
+
message,
|
|
39
|
+
code,
|
|
40
|
+
fields,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
// Imports / modules
|
|
4
|
+
// "bundler" is THE setting that lets you import WITHOUT extensions:
|
|
5
|
+
// import app from "./app" (not "./app.ts")
|
|
6
|
+
"module": "esnext", // emit modern ES module syntax
|
|
7
|
+
"moduleResolution": "bundler", // resolve imports without ".ts" extensions
|
|
8
|
+
"moduleDetection": "force", // treat every file as an ES module (no CommonJS guessing)
|
|
9
|
+
"esModuleInterop": true, // allow normal default imports from CJS packages (express)
|
|
10
|
+
"verbatimModuleSyntax": true, // keep imports exactly as written; type-only imports must use "import type"
|
|
11
|
+
|
|
12
|
+
// Language level
|
|
13
|
+
"target": "esnext", // allow the newest JS features, no downleveling
|
|
14
|
+
|
|
15
|
+
// Type checking
|
|
16
|
+
"strict": true, // turn on all strict type checks
|
|
17
|
+
"noUnusedLocals": true, // error on declared-but-unused variables
|
|
18
|
+
"noUnusedParameters": true, // error on unused function parameters
|
|
19
|
+
"skipLibCheck": true, // don't type-check ".d.ts" files inside node_modules (faster)
|
|
20
|
+
|
|
21
|
+
// Runtime types
|
|
22
|
+
"types": ["node"], // auto-include ONLY Node types (no stray umbrella types)
|
|
23
|
+
|
|
24
|
+
// Output
|
|
25
|
+
"noEmit": true // tsx runs the code directly, so tsc only type-checks, never writes files
|
|
26
|
+
},
|
|
27
|
+
// Files in scope
|
|
28
|
+
"include": ["src"], // the compiler only looks at src/
|
|
29
|
+
"exclude": ["node_modules"] // never scan dependencies
|
|
30
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import express, { type Request, type Response } from "express";
|
|
2
|
+
import errorMiddleware from "./middleware/error.middleware";
|
|
3
|
+
import notFoundMiddleware from "./middleware/notFound.middleware";
|
|
4
|
+
import { getTimeStamp } from "./utils/helpers";
|
|
5
|
+
import { successResponse } from "./utils/responseHelpers";
|
|
6
|
+
|
|
7
|
+
const app = express();
|
|
8
|
+
|
|
9
|
+
app.use(express.json());
|
|
10
|
+
|
|
11
|
+
// Health check
|
|
12
|
+
app.get("/health", (_req: Request, res: Response) =>
|
|
13
|
+
successResponse(res, {
|
|
14
|
+
statusCode: 200,
|
|
15
|
+
message: "API is up and running",
|
|
16
|
+
data: {
|
|
17
|
+
status: "ok",
|
|
18
|
+
uptime: Math.round(process.uptime()),
|
|
19
|
+
timestamp: getTimeStamp(),
|
|
20
|
+
},
|
|
21
|
+
}),
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
// Unknown routes → 404, then every error gets formatted in one place.
|
|
25
|
+
app.use(notFoundMiddleware);
|
|
26
|
+
app.use(errorMiddleware);
|
|
27
|
+
|
|
28
|
+
export default app;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { NextFunction, Request, Response } from "express";
|
|
2
|
+
import { AppError, BadRequestError } from "../utils/AppError";
|
|
3
|
+
import { errorResponse } from "../utils/responseHelpers";
|
|
4
|
+
import { getTimeStamp } from "../utils/helpers";
|
|
5
|
+
|
|
6
|
+
// Express error handler — needs all 4 params, that's how Express recognizes it.
|
|
7
|
+
export default function errorMiddleware(
|
|
8
|
+
error: unknown,
|
|
9
|
+
req: Request,
|
|
10
|
+
res: Response,
|
|
11
|
+
_next: NextFunction,
|
|
12
|
+
): Response {
|
|
13
|
+
const timeStamp = getTimeStamp();
|
|
14
|
+
|
|
15
|
+
// Expected errors — log them and send their details
|
|
16
|
+
if (error instanceof AppError) {
|
|
17
|
+
console.log(
|
|
18
|
+
`[${timeStamp}] [ERROR] ${req.method} ${req.originalUrl} - ${error.message}`,
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
return errorResponse(res, {
|
|
22
|
+
statusCode: error.statusCode,
|
|
23
|
+
message: error.message,
|
|
24
|
+
code: error.code,
|
|
25
|
+
fields: error instanceof BadRequestError ? error.fields : undefined, // fields only for bad requests
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Not an AppError — build a message if it's an Error, otherwise fall back
|
|
30
|
+
const message =
|
|
31
|
+
error instanceof Error ? error.message : "Internal server error";
|
|
32
|
+
|
|
33
|
+
console.error(
|
|
34
|
+
`[${timeStamp}] [ERROR] ${req.method} ${req.originalUrl} - ${message}`,
|
|
35
|
+
error,
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
return errorResponse(res, {
|
|
39
|
+
statusCode: 500,
|
|
40
|
+
message,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { NextFunction, Request, Response } from "express";
|
|
2
|
+
import { NotFoundError } from "../utils/AppError";
|
|
3
|
+
|
|
4
|
+
export default function notFoundMiddleware(
|
|
5
|
+
_req: Request,
|
|
6
|
+
_res: Response,
|
|
7
|
+
next: NextFunction,
|
|
8
|
+
): void {
|
|
9
|
+
next(new NotFoundError("Route not found"));
|
|
10
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { FieldError } from "../utils/responseHelpers";
|
|
2
|
+
|
|
3
|
+
export type AppErrorOptions = {
|
|
4
|
+
code?: string;
|
|
5
|
+
statusCode?: number;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export class AppError extends Error {
|
|
9
|
+
public readonly code: string;
|
|
10
|
+
public readonly statusCode: number;
|
|
11
|
+
|
|
12
|
+
constructor(
|
|
13
|
+
message: string,
|
|
14
|
+
{ code = "INTERNAL_ERROR", statusCode = 500 }: AppErrorOptions = {},
|
|
15
|
+
) {
|
|
16
|
+
super(message);
|
|
17
|
+
|
|
18
|
+
this.name = this.constructor.name;
|
|
19
|
+
this.code = code; // machine-readable code, e.g. "BAD_REQUEST"
|
|
20
|
+
this.statusCode = statusCode; // HTTP status to send back
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class BadRequestError extends AppError {
|
|
25
|
+
public readonly fields: FieldError[]; // only this error carries fields
|
|
26
|
+
|
|
27
|
+
constructor(message = "Bad request", fields: FieldError[] = []) {
|
|
28
|
+
super(message, { code: "BAD_REQUEST", statusCode: 400 });
|
|
29
|
+
|
|
30
|
+
this.fields = fields;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export class UnauthorizedError extends AppError {
|
|
35
|
+
constructor(message = "Unauthorized") {
|
|
36
|
+
super(message, { code: "UNAUTHORIZED", statusCode: 401 });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class NotFoundError extends AppError {
|
|
41
|
+
constructor(message = "Not found") {
|
|
42
|
+
super(message, { code: "NOT_FOUND", statusCode: 404 });
|
|
43
|
+
}
|
|
44
|
+
}
|