better-ts-stack 2.0.2 → 2.1.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 +114 -113
- package/dist/args.js +27 -20
- package/dist/builder/buildPlanner.js +1 -1
- package/dist/builder/configGenerator.js +4 -6
- package/dist/builder/index.js +15 -3
- package/dist/builder/moduleSelector.js +6 -2
- package/dist/builder/readmeGenerator.js +107 -0
- package/dist/builder/templateContext.js +53 -0
- package/dist/index.js +7 -1
- package/dist/output/nextSteps.js +23 -22
- package/dist/prompts/index.js +5 -2
- package/dist/prompts/stack.js +23 -2
- package/dist/types/index.js +2 -0
- package/dist/ui/messages.js +3 -1
- package/dist/validators/index.js +5 -2
- package/package.json +3 -3
- package/templates/backend/express/config.json +42 -40
- package/templates/backend/express/gitignore +3 -0
- package/templates/backend/express/src/env.ts.hbs +39 -0
- package/templates/backend/express/src/index.ts.hbs +111 -75
- package/templates/backend/nestjs/config.json +43 -41
- package/templates/backend/nestjs/gitignore +3 -0
- package/templates/backend/nestjs/src/app.module.ts.hbs +13 -0
- package/templates/backend/nestjs/src/env.ts.hbs +39 -0
- package/templates/backend/nestjs/src/health/{health.controller.ts → health.controller.ts.hbs} +8 -0
- package/templates/backend/nestjs/src/main.ts +3 -2
- package/templates/frontend/nextjs/app/page.tsx.hbs +98 -97
- package/templates/frontend/nextjs/config.json +12 -14
- package/templates/frontend/nextjs/gitignore +3 -0
- package/templates/frontend/tanstack/config.json +16 -16
- package/templates/frontend/tanstack/gitignore +3 -0
- package/templates/frontend/tanstack/src/routes/index.tsx.hbs +102 -101
- package/templates/frontend/tanstack/vite.config.ts +23 -14
- package/templates/modules/auth/express/config.json +20 -20
- package/templates/modules/auth/express/src/middleware/requireAuth.ts.hbs +1 -2
- package/templates/modules/auth/express/src/routes/auth.ts.hbs +27 -19
- package/templates/modules/auth/nestjs/config.json +20 -20
- package/templates/modules/auth/nestjs/src/auth/{auth.controller.ts → auth.controller.ts.hbs} +15 -0
- package/templates/modules/auth/nextjs/config.json +4 -4
- package/templates/{frontend/nextjs/proxy.ts.hbs → modules/auth/nextjs/proxy.ts} +1 -8
- package/templates/modules/auth/tanstack/config.json +4 -4
- package/templates/modules/auth/tanstack/src/components/auth/sign-in-form.tsx +132 -134
- package/templates/modules/auth/tanstack/src/components/auth/sign-out-button.tsx +46 -48
- package/templates/modules/auth/tanstack/src/components/auth/sign-up-form.tsx +150 -152
- package/templates/modules/ci/.github/workflows/ci.yml.hbs +43 -0
- package/templates/modules/ci/config.json +10 -0
- package/templates/modules/docker/.dockerignore +2 -0
- package/templates/modules/docker/Dockerfile.hbs +147 -136
- package/templates/modules/docker/docker-compose.yml.hbs +97 -2
- package/templates/modules/drizzle/express/config.json +3 -3
- package/templates/modules/drizzle/nestjs/config.json +3 -3
- package/templates/modules/drizzle/nextjs/config.json +3 -3
- package/templates/modules/drizzle/tanstack/config.json +3 -3
- package/templates/modules/mongoose/express/config.json +2 -2
- package/templates/modules/mongoose/nestjs/config.json +1 -1
- package/templates/modules/mongoose/nextjs/config.json +2 -2
- package/templates/modules/mongoose/tanstack/config.json +2 -2
- package/templates/modules/prisma/express/config.json +4 -4
- package/templates/modules/prisma/express/prisma.config.ts +4 -2
- package/templates/modules/prisma/nestjs/config.json +4 -4
- package/templates/modules/prisma/nestjs/prisma.config.ts +4 -2
- package/templates/modules/prisma/nextjs/config.json +4 -4
- package/templates/modules/prisma/nextjs/prisma.config.ts +4 -2
- package/templates/modules/prisma/tanstack/config.json +4 -4
- package/templates/modules/prisma/tanstack/prisma.config.ts +4 -2
- package/templates/modules/rate-limit/express/config.json +12 -0
- package/templates/modules/rate-limit/express/src/middleware/rateLimit.ts +23 -0
- package/templates/modules/rate-limit/nestjs/config.json +12 -0
package/dist/output/nextSteps.js
CHANGED
|
@@ -7,23 +7,16 @@ exports.generateNextSteps = generateNextSteps;
|
|
|
7
7
|
const node_path_1 = __importDefault(require("node:path"));
|
|
8
8
|
const node_process_1 = require("node:process");
|
|
9
9
|
const MONGODB_AUTH_SETUP_URL = "https://better-ts-stack.abdullahtech.me/docs/modules/database#mongoose-and-mongodb";
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
|
|
13
|
-
function
|
|
14
|
-
const [currentMajor, currentMinor] = process.versions.node
|
|
15
|
-
.split(".")
|
|
16
|
-
.map(Number);
|
|
17
|
-
return (currentMajor < major || (currentMajor === major && currentMinor < minor));
|
|
18
|
-
}
|
|
19
|
-
// Generates a list of instructions for the user to follow after project creation
|
|
20
|
-
function generateNextSteps(config, targetDir, depsInstalled) {
|
|
10
|
+
// Generates a list of instructions for the user to follow after project
|
|
11
|
+
// creation. The README version leaves out the cd step, which only makes
|
|
12
|
+
// sense in the terminal
|
|
13
|
+
function generateNextSteps(config, targetDir, depsInstalled, isForReadme = false) {
|
|
21
14
|
const steps = [];
|
|
22
15
|
const isFullstack = config.applicationType === "fullstack";
|
|
23
16
|
// Step 1: cd into project directory, unless it was created in place
|
|
24
17
|
// Quoted when it has spaces, so the command can be pasted as it is
|
|
25
18
|
const relativeDir = node_path_1.default.relative((0, node_process_1.cwd)(), targetDir);
|
|
26
|
-
if (relativeDir) {
|
|
19
|
+
if (relativeDir && !isForReadme) {
|
|
27
20
|
steps.push(/\s/.test(relativeDir) ? `cd "${relativeDir}"` : `cd ${relativeDir}`);
|
|
28
21
|
}
|
|
29
22
|
// Step 2: Set environment variables
|
|
@@ -35,17 +28,19 @@ function generateNextSteps(config, targetDir, depsInstalled) {
|
|
|
35
28
|
? "Review BETTER_AUTH_SECRET and BETTER_AUTH_URL in .env"
|
|
36
29
|
: "Review the generated JWT_SECRET in .env");
|
|
37
30
|
}
|
|
38
|
-
if (config.framework === "tanstack" &&
|
|
39
|
-
process.platform === "win32" &&
|
|
40
|
-
isNodeBelow(MIN_TANSTACK_WINDOWS_NODE)) {
|
|
41
|
-
steps.push(`Upgrade Node.js to ${MIN_TANSTACK_WINDOWS_NODE.join(".")} or newer: on Windows, ` +
|
|
42
|
-
`Node ${process.versions.node} makes "npm start" return 404 for CSS, JS, and other static files`);
|
|
43
|
-
}
|
|
44
31
|
// Step 3: Install dependencies if not already done
|
|
45
32
|
if (!depsInstalled) {
|
|
46
33
|
steps.push(`${config.packageManager} install`);
|
|
47
34
|
}
|
|
48
|
-
// Step 4: Database-specific steps
|
|
35
|
+
// Step 4: Database-specific steps. With Docker, Compose runs the database
|
|
36
|
+
const hasDockerDatabase = config.useDocker && config.database !== "none";
|
|
37
|
+
if (hasDockerDatabase) {
|
|
38
|
+
steps.push("docker compose up -d db");
|
|
39
|
+
}
|
|
40
|
+
if ((config.database === "prisma" || config.database === "drizzle") &&
|
|
41
|
+
!hasDockerDatabase) {
|
|
42
|
+
steps.push("Ensure PostgreSQL is running locally or update DATABASE_URL in .env");
|
|
43
|
+
}
|
|
49
44
|
if (config.database === "prisma") {
|
|
50
45
|
steps.push(`${config.packageManager} run prisma:generate`);
|
|
51
46
|
steps.push(`${config.packageManager} run prisma:migrate`);
|
|
@@ -55,9 +50,12 @@ function generateNextSteps(config, targetDir, depsInstalled) {
|
|
|
55
50
|
steps.push(`${config.packageManager} run db:migrate`);
|
|
56
51
|
}
|
|
57
52
|
else if (config.database === "mongoose") {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
53
|
+
if (!hasDockerDatabase) {
|
|
54
|
+
steps.push("Ensure MongoDB is running locally or update MONGODB_URI in .env");
|
|
55
|
+
}
|
|
56
|
+
// Better Auth's MongoDB adapter uses transactions, which need a replica
|
|
57
|
+
// set; the Compose database already runs as one
|
|
58
|
+
if (config.useAuth && isFullstack && !hasDockerDatabase) {
|
|
61
59
|
steps.push("Better Auth uses MongoDB transactions: run MongoDB as a replica set (or use Atlas) " +
|
|
62
60
|
'and create the "user", "session", "account", and "verification" collections before signing up. ' +
|
|
63
61
|
`See ${MONGODB_AUTH_SETUP_URL}`);
|
|
@@ -70,5 +68,8 @@ function generateNextSteps(config, targetDir, depsInstalled) {
|
|
|
70
68
|
? "Visit /sign-up to create your first account"
|
|
71
69
|
: "Create a user via POST /auth/register then login with /auth/login");
|
|
72
70
|
}
|
|
71
|
+
if (config.useDocker) {
|
|
72
|
+
steps.push(`To run the app in Docker instead: ${config.packageManager} run docker:up`);
|
|
73
|
+
}
|
|
73
74
|
return steps;
|
|
74
75
|
}
|
package/dist/prompts/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.collectStackChoices = void 0;
|
|
3
|
+
exports.detectPackageManager = exports.collectStackChoices = void 0;
|
|
4
4
|
exports.collectProjectPath = collectProjectPath;
|
|
5
5
|
exports.confirmBuild = confirmBuild;
|
|
6
6
|
const types_1 = require("../types");
|
|
@@ -9,6 +9,7 @@ const validators_1 = require("../validators");
|
|
|
9
9
|
const stack_1 = require("./stack");
|
|
10
10
|
var stack_2 = require("./stack");
|
|
11
11
|
Object.defineProperty(exports, "collectStackChoices", { enumerable: true, get: function () { return stack_2.collectStackChoices; } });
|
|
12
|
+
Object.defineProperty(exports, "detectPackageManager", { enumerable: true, get: function () { return stack_2.detectPackageManager; } });
|
|
12
13
|
// Returns the project path from the command line, or asks for it
|
|
13
14
|
async function collectProjectPath(preset, isNonInteractive) {
|
|
14
15
|
if (preset !== undefined) {
|
|
@@ -22,13 +23,15 @@ async function collectProjectPath(preset, isNonInteractive) {
|
|
|
22
23
|
return (0, stack_1.ask)(preset, () => (0, ui_1.text)({
|
|
23
24
|
message: "Project name",
|
|
24
25
|
placeholder: "my-awesome-project",
|
|
25
|
-
|
|
26
|
+
// An empty path would resolve to the current directory and pass
|
|
27
|
+
validate: (value) => value ? (0, validators_1.validateProjectPath)(value) : "Project name is required",
|
|
26
28
|
}));
|
|
27
29
|
}
|
|
28
30
|
// Asks for final confirmation before building; declining ends the session
|
|
29
31
|
async function confirmBuild() {
|
|
30
32
|
const shouldBuild = await (0, stack_1.ask)(undefined, () => (0, ui_1.confirm)({ message: "Create this project?", initialValue: true }));
|
|
31
33
|
if (!shouldBuild) {
|
|
34
|
+
(0, ui_1.info)("Nothing was created.");
|
|
32
35
|
process.exit(0);
|
|
33
36
|
}
|
|
34
37
|
}
|
package/dist/prompts/stack.js
CHANGED
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.detectPackageManager = detectPackageManager;
|
|
3
4
|
exports.ask = ask;
|
|
4
5
|
exports.collectStackChoices = collectStackChoices;
|
|
5
6
|
const core_1 = require("@clack/core");
|
|
6
7
|
const types_1 = require("../types");
|
|
7
8
|
const ui_1 = require("../ui");
|
|
9
|
+
// The package manager that started the CLI (pnpm dlx, bunx, npx), from the
|
|
10
|
+
// user agent it passes to the process; npm when it cannot be told
|
|
11
|
+
function detectPackageManager() {
|
|
12
|
+
const userAgent = process.env.npm_config_user_agent ?? "";
|
|
13
|
+
const name = userAgent.split("/")[0];
|
|
14
|
+
return types_1.packageManagerOptions.some((option) => option.value === name)
|
|
15
|
+
? name
|
|
16
|
+
: "npm";
|
|
17
|
+
}
|
|
8
18
|
// Returns the preset value when there is one, otherwise asks the user
|
|
9
19
|
async function ask(preset, prompt) {
|
|
10
20
|
if (preset !== undefined)
|
|
@@ -16,7 +26,8 @@ async function ask(preset, prompt) {
|
|
|
16
26
|
const answer = await prompt();
|
|
17
27
|
if ((0, core_1.isCancel)(answer)) {
|
|
18
28
|
(0, ui_1.info)("Cancelled. Nothing was created.");
|
|
19
|
-
|
|
29
|
+
// 130 is the usual exit code for a run stopped with Ctrl+C
|
|
30
|
+
process.exit(130);
|
|
20
31
|
}
|
|
21
32
|
return answer;
|
|
22
33
|
}
|
|
@@ -62,7 +73,7 @@ async function collectStackChoices(preset) {
|
|
|
62
73
|
const packageManager = await ask(preset.packageManager, () => (0, ui_1.select)({
|
|
63
74
|
message: "Package manager",
|
|
64
75
|
options: types_1.packageManagerOptions,
|
|
65
|
-
initialValue:
|
|
76
|
+
initialValue: detectPackageManager(),
|
|
66
77
|
}));
|
|
67
78
|
const useDocker = await ask(preset.useDocker, () => (0, ui_1.confirm)({ message: "Add Docker?", initialValue: false }));
|
|
68
79
|
// Better Auth stores users and sessions, so full-stack auth needs a database
|
|
@@ -77,6 +88,14 @@ async function collectStackChoices(preset) {
|
|
|
77
88
|
initialValue: false,
|
|
78
89
|
}))
|
|
79
90
|
: false;
|
|
91
|
+
// Better Auth limits its own endpoints, so only the backends offer this
|
|
92
|
+
if (preset.useRateLimit && isFullstack) {
|
|
93
|
+
throw (0, types_1.buildError)(`Rate limiting is available for Express and NestJS. Better Auth in ${framework} already limits its own endpoints`, "INVALID_ARGUMENT");
|
|
94
|
+
}
|
|
95
|
+
const useRateLimit = isFullstack
|
|
96
|
+
? false
|
|
97
|
+
: await ask(preset.useRateLimit, () => (0, ui_1.confirm)({ message: "Add rate limiting?", initialValue: false }));
|
|
98
|
+
const useCi = await ask(preset.useCi, () => (0, ui_1.confirm)({ message: "Add GitHub Actions CI?", initialValue: false }));
|
|
80
99
|
const initGit = await ask(preset.initGit, () => (0, ui_1.confirm)({ message: "Initialize a git repository?", initialValue: true }));
|
|
81
100
|
const installDeps = await ask(preset.installDeps, () => (0, ui_1.confirm)({ message: "Install dependencies now?", initialValue: false }));
|
|
82
101
|
return {
|
|
@@ -85,6 +104,8 @@ async function collectStackChoices(preset) {
|
|
|
85
104
|
packageManager,
|
|
86
105
|
useDocker,
|
|
87
106
|
useAuth,
|
|
107
|
+
useRateLimit,
|
|
108
|
+
useCi,
|
|
88
109
|
initGit,
|
|
89
110
|
installDeps,
|
|
90
111
|
};
|
package/dist/types/index.js
CHANGED
package/dist/ui/messages.js
CHANGED
|
@@ -73,7 +73,9 @@ function showSummary(config, targetDir) {
|
|
|
73
73
|
config.framework,
|
|
74
74
|
config.database !== "none" && config.database,
|
|
75
75
|
config.useAuth && "auth",
|
|
76
|
+
config.useRateLimit && "rate limit",
|
|
76
77
|
config.useDocker && "docker",
|
|
78
|
+
config.useCi && "ci",
|
|
77
79
|
];
|
|
78
80
|
const setup = [
|
|
79
81
|
config.packageManager,
|
|
@@ -115,7 +117,7 @@ function showNextSteps(steps) {
|
|
|
115
117
|
write();
|
|
116
118
|
write(` ${(0, theme_1.amber)((0, theme_1.bold)("Next steps"))}`);
|
|
117
119
|
steps.forEach((step, index) => {
|
|
118
|
-
const isCommand = /^(cd|npm|pnpm|bun) /.test(step);
|
|
120
|
+
const isCommand = /^(cd|npm|pnpm|bun|docker) /.test(step);
|
|
119
121
|
write(` ${(0, theme_1.dim)(`${index + 1}.`)} ${isCommand ? (0, theme_1.lime)(step) : step}`);
|
|
120
122
|
});
|
|
121
123
|
write();
|
package/dist/validators/index.js
CHANGED
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.projectNameSchema = void 0;
|
|
6
|
+
exports.PRESERVED_ENTRIES = exports.projectNameSchema = void 0;
|
|
7
7
|
exports.validateProjectName = validateProjectName;
|
|
8
8
|
exports.validateProjectPath = validateProjectPath;
|
|
9
9
|
exports.validateDirectoryEmpty = validateDirectoryEmpty;
|
|
@@ -32,6 +32,9 @@ function validateProjectName(name) {
|
|
|
32
32
|
function validateProjectPath(projectPath) {
|
|
33
33
|
return validateProjectName(node_path_1.default.basename(node_path_1.default.resolve(projectPath)));
|
|
34
34
|
}
|
|
35
|
+
// Entries a project folder may already have: a repository cloned from an
|
|
36
|
+
// empty remote, and files the operating system creates
|
|
37
|
+
exports.PRESERVED_ENTRIES = [".git", ".DS_Store", "Thumbs.db"];
|
|
35
38
|
// Ensures a directory is empty or does not exist before starting build
|
|
36
39
|
async function validateDirectoryEmpty(dirPath) {
|
|
37
40
|
const exists = await fs_extra_1.default.pathExists(dirPath);
|
|
@@ -42,7 +45,7 @@ async function validateDirectoryEmpty(dirPath) {
|
|
|
42
45
|
return `Path "${dirPath}" exists but is not a directory`;
|
|
43
46
|
}
|
|
44
47
|
const files = await fs_extra_1.default.readdir(dirPath);
|
|
45
|
-
if (files.
|
|
48
|
+
if (files.every((file) => exports.PRESERVED_ENTRIES.includes(file)))
|
|
46
49
|
return null;
|
|
47
50
|
return `Directory "${node_path_1.default.basename(dirPath)}" already exists and is not empty`;
|
|
48
51
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "better-ts-stack",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.1",
|
|
4
4
|
"description": "A CLI that scaffolds Express, NestJS, Next.js, and TanStack Start projects with a database, authentication, and Docker already set up.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -51,8 +51,8 @@
|
|
|
51
51
|
"dependencies": {
|
|
52
52
|
"@clack/core": "^1.4.3",
|
|
53
53
|
"fs-extra": "^11.4.0",
|
|
54
|
-
"handlebars": "^4.7.
|
|
55
|
-
"zod": "^4.
|
|
54
|
+
"handlebars": "^4.7.10",
|
|
55
|
+
"zod": "^4.6.7"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
58
|
"@better-ts-stack/eslint-config": "*",
|
|
@@ -1,40 +1,42 @@
|
|
|
1
|
-
{
|
|
2
|
-
"id": "express",
|
|
3
|
-
"name": "Express Base",
|
|
4
|
-
"description": "Base Express server with TypeScript, security headers, and a health check",
|
|
5
|
-
"type": "base",
|
|
6
|
-
"scripts": {
|
|
7
|
-
"dev": "cross-env NODE_ENV=development {{#if (eq packageManager 'bun')}}bun --watch src/index.ts{{else}}tsx watch src/index.ts{{/if}}",
|
|
8
|
-
"build": "tsc",
|
|
9
|
-
"start": "cross-env NODE_ENV=production {{runner}} dist/index.js",
|
|
10
|
-
"lint": "eslint src",
|
|
11
|
-
"lint:fix": "eslint src --fix",
|
|
12
|
-
"format": "prettier --write \"src/**/*.ts\"",
|
|
13
|
-
"type:check": "tsc --noEmit"
|
|
14
|
-
},
|
|
15
|
-
"dependencies": {
|
|
16
|
-
"express": "^5.
|
|
17
|
-
"dotenv": "^17.4.2",
|
|
18
|
-
"cors": "^2.8.6",
|
|
19
|
-
"cross-env": "^10.1.0",
|
|
20
|
-
"helmet": "^8.3.0",
|
|
21
|
-
"morgan": "^1.12.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
"@
|
|
26
|
-
"@
|
|
27
|
-
"@types/
|
|
28
|
-
"@types/
|
|
29
|
-
"@types/
|
|
30
|
-
"
|
|
31
|
-
"eslint
|
|
32
|
-
"prettier": "^
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"typescript
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
1
|
+
{
|
|
2
|
+
"id": "express",
|
|
3
|
+
"name": "Express Base",
|
|
4
|
+
"description": "Base Express server with TypeScript, security headers, and a health check",
|
|
5
|
+
"type": "base",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "cross-env NODE_ENV=development {{#if (eq packageManager 'bun')}}bun --watch src/index.ts{{else}}tsx watch src/index.ts{{/if}}",
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"start": "cross-env NODE_ENV=production {{runner}} dist/index.js",
|
|
10
|
+
"lint": "eslint src",
|
|
11
|
+
"lint:fix": "eslint src --fix",
|
|
12
|
+
"format": "prettier --write \"src/**/*.ts\"",
|
|
13
|
+
"type:check": "tsc --noEmit"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"express": "^5.3.0",
|
|
17
|
+
"dotenv": "^17.4.2",
|
|
18
|
+
"cors": "^2.8.6",
|
|
19
|
+
"cross-env": "^10.1.0",
|
|
20
|
+
"helmet": "^8.3.0",
|
|
21
|
+
"morgan": "^1.12.1",
|
|
22
|
+
"zod": "^4.6.7"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@eslint/js": "^10.0.1",
|
|
26
|
+
"@trivago/prettier-plugin-sort-imports": "^6.0.2",
|
|
27
|
+
"@types/cors": "^2.8.19",
|
|
28
|
+
"@types/express": "^5.0.6",
|
|
29
|
+
"@types/morgan": "^1.9.10",
|
|
30
|
+
"@types/node": "^24.19.2",
|
|
31
|
+
"eslint": "^10.14.0",
|
|
32
|
+
"eslint-config-prettier": "^10.1.8",
|
|
33
|
+
"prettier": "^3.9.10",
|
|
34
|
+
"tsx": "^4.23.15",
|
|
35
|
+
"typescript": "^6.0.3",
|
|
36
|
+
"typescript-eslint": "^8.72.1"
|
|
37
|
+
},
|
|
38
|
+
"envVars": {
|
|
39
|
+
"PORT": "3000",
|
|
40
|
+
"CORS_ORIGIN": "*"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import "dotenv/config";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
// Checked once at startup, so a missing or malformed variable stops the server
|
|
5
|
+
// with a clear message instead of failing on the first request that needs it
|
|
6
|
+
const envSchema = z.object({
|
|
7
|
+
PORT: z.coerce.number().int().positive().default(3000),
|
|
8
|
+
// "*" allows every origin; otherwise a comma-separated list of origins
|
|
9
|
+
CORS_ORIGIN: z
|
|
10
|
+
.string()
|
|
11
|
+
.default("*")
|
|
12
|
+
.transform((value) =>
|
|
13
|
+
value === "*" ? value : value.split(",").map((origin) => origin.trim())
|
|
14
|
+
),
|
|
15
|
+
{{#if (eq database 'prisma')}}
|
|
16
|
+
DATABASE_URL: z.url(),
|
|
17
|
+
{{/if}}
|
|
18
|
+
{{#if (eq database 'drizzle')}}
|
|
19
|
+
DATABASE_URL: z.url(),
|
|
20
|
+
{{/if}}
|
|
21
|
+
{{#if (eq database 'mongoose')}}
|
|
22
|
+
MONGODB_URI: z.url(),
|
|
23
|
+
{{/if}}
|
|
24
|
+
{{#if useAuth}}
|
|
25
|
+
JWT_SECRET: z.string().min(32, "JWT_SECRET must be at least 32 characters"),
|
|
26
|
+
JWT_EXPIRES_IN: z.string().default("1h"),
|
|
27
|
+
{{/if}}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const result = envSchema.safeParse(process.env);
|
|
31
|
+
|
|
32
|
+
if (!result.success) {
|
|
33
|
+
console.error(
|
|
34
|
+
`Invalid environment variables:\n${z.prettifyError(result.error)}`
|
|
35
|
+
);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const env = result.data;
|
|
@@ -1,75 +1,111 @@
|
|
|
1
|
-
import "dotenv/config";
|
|
2
|
-
import express, { Express, Request, Response, NextFunction } from "express";
|
|
3
|
-
import cors from "cors";
|
|
4
|
-
import helmet from "helmet";
|
|
5
|
-
import morgan from "morgan";
|
|
6
|
-
{
|
|
7
|
-
|
|
8
|
-
{
|
|
9
|
-
|
|
10
|
-
{{#if
|
|
11
|
-
import
|
|
12
|
-
{{/if}}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
app.use(
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
{{
|
|
72
|
-
|
|
73
|
-
{{
|
|
74
|
-
|
|
75
|
-
|
|
1
|
+
import "dotenv/config";
|
|
2
|
+
import express, { Express, Request, Response, NextFunction } from "express";
|
|
3
|
+
import cors from "cors";
|
|
4
|
+
import helmet from "helmet";
|
|
5
|
+
import morgan from "morgan";
|
|
6
|
+
import { env } from "./env";
|
|
7
|
+
{{#if (eq database 'prisma')}}
|
|
8
|
+
import { disconnectPrisma } from "./lib/prisma";
|
|
9
|
+
{{/if}}
|
|
10
|
+
{{#if (eq database 'drizzle')}}
|
|
11
|
+
import { db } from "./lib/db";
|
|
12
|
+
{{/if}}
|
|
13
|
+
{{#if (eq database 'mongoose')}}
|
|
14
|
+
import { connectDB, disconnectDB } from "./lib/db";
|
|
15
|
+
{{/if}}
|
|
16
|
+
{{#if useRateLimit}}
|
|
17
|
+
import { apiLimiter } from "./middleware/rateLimit";
|
|
18
|
+
{{/if}}
|
|
19
|
+
import healthRouter from "./routes/health";
|
|
20
|
+
{{#if useAuth}}
|
|
21
|
+
import authRouter from "./routes/auth";
|
|
22
|
+
{{/if}}
|
|
23
|
+
|
|
24
|
+
const app: Express = express();
|
|
25
|
+
const port = env.PORT;
|
|
26
|
+
|
|
27
|
+
// Middleware
|
|
28
|
+
app.use(helmet());
|
|
29
|
+
app.use(cors({ origin: env.CORS_ORIGIN }));
|
|
30
|
+
{{#if useRateLimit}}
|
|
31
|
+
app.use(apiLimiter);
|
|
32
|
+
{{/if}}
|
|
33
|
+
app.use(morgan("dev"));
|
|
34
|
+
app.use(express.json());
|
|
35
|
+
app.use(express.urlencoded({ extended: true }));
|
|
36
|
+
|
|
37
|
+
// Routes
|
|
38
|
+
app.use("/health", healthRouter);
|
|
39
|
+
{{#if useAuth}}
|
|
40
|
+
app.use("/auth", authRouter);
|
|
41
|
+
{{/if}}
|
|
42
|
+
|
|
43
|
+
// 404 handler
|
|
44
|
+
app.use((_req: Request, res: Response) => {
|
|
45
|
+
res.status(404).json({ error: "Not Found" });
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Error handler
|
|
49
|
+
app.use(
|
|
50
|
+
(
|
|
51
|
+
err: Error & { status?: number; statusCode?: number; expose?: boolean },
|
|
52
|
+
_req: Request,
|
|
53
|
+
res: Response,
|
|
54
|
+
_next: NextFunction,
|
|
55
|
+
) => {
|
|
56
|
+
// Middleware such as express.json() sets 4xx statuses (bad JSON, body too large)
|
|
57
|
+
const status = err.status ?? err.statusCode ?? 500;
|
|
58
|
+
|
|
59
|
+
if (status >= 400 && status < 500) {
|
|
60
|
+
res.status(status).json({ error: err.expose ? err.message : "Bad Request" });
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
console.error(err.stack);
|
|
65
|
+
res.status(500).json({ error: "Internal Server Error" });
|
|
66
|
+
},
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
// Closes the database connection once the server has stopped
|
|
70
|
+
function closeDatabase(): Promise<void> {
|
|
71
|
+
{{#if (eq database 'prisma')}}
|
|
72
|
+
return disconnectPrisma();
|
|
73
|
+
{{else if (eq database 'drizzle')}}
|
|
74
|
+
return db.$client.end();
|
|
75
|
+
{{else if (eq database 'mongoose')}}
|
|
76
|
+
return disconnectDB();
|
|
77
|
+
{{else}}
|
|
78
|
+
return Promise.resolve();
|
|
79
|
+
{{/if}}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function listen(): void {
|
|
83
|
+
const server = app.listen(port, () => {
|
|
84
|
+
console.log(`Server is running on http://localhost:${port}
|
|
85
|
+
Health: http://localhost:${port}/health
|
|
86
|
+
`);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Docker and most hosts stop the process with SIGTERM, and Ctrl+C sends
|
|
90
|
+
// SIGINT: finish open requests, close the database, then exit
|
|
91
|
+
const shutdown = () => {
|
|
92
|
+
server.close(() => {
|
|
93
|
+
void closeDatabase().finally(() => process.exit(0));
|
|
94
|
+
});
|
|
95
|
+
};
|
|
96
|
+
process.once("SIGTERM", shutdown);
|
|
97
|
+
process.once("SIGINT", shutdown);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
{{#if (eq database 'mongoose')}}
|
|
101
|
+
void connectDB()
|
|
102
|
+
.then(listen)
|
|
103
|
+
.catch((error: unknown) => {
|
|
104
|
+
console.error(error);
|
|
105
|
+
process.exitCode = 1;
|
|
106
|
+
});
|
|
107
|
+
{{else}}
|
|
108
|
+
listen();
|
|
109
|
+
{{/if}}
|
|
110
|
+
|
|
111
|
+
export default app;
|