better-ts-stack 2.0.1 → 2.1.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 +114 -113
- package/dist/args.js +24 -20
- package/dist/builder/buildPlanner.js +1 -1
- package/dist/builder/configGenerator.js +1 -5
- package/dist/builder/index.js +15 -3
- package/dist/builder/moduleSelector.js +4 -2
- package/dist/builder/readmeGenerator.js +101 -0
- package/dist/builder/templateContext.js +30 -0
- package/dist/index.js +7 -1
- package/dist/output/nextSteps.js +22 -23
- package/dist/prompts/index.js +2 -1
- package/dist/prompts/stack.js +21 -2
- package/dist/types/index.js +2 -1
- package/dist/ui/messages.js +2 -1
- package/dist/validators/index.js +5 -2
- package/package.json +1 -1
- package/templates/backend/express/config.json +42 -40
- 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/src/app.module.ts.hbs +13 -0
- package/templates/backend/nestjs/src/env.ts.hbs +39 -0
- package/templates/backend/nestjs/src/main.ts +3 -2
- package/templates/frontend/nextjs/app/page.tsx.hbs +98 -97
- package/templates/frontend/tanstack/config.json +45 -45
- package/templates/frontend/tanstack/src/routes/index.tsx.hbs +102 -101
- package/templates/frontend/tanstack/vite.config.ts +22 -14
- package/templates/modules/auth/express/config.json +20 -20
- 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/lib/auth-schema.ts +1 -1
- 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/auth/tanstack/src/lib/auth-schema.ts +38 -38
- package/templates/modules/docker/.dockerignore +1 -0
- package/templates/modules/docker/Dockerfile.hbs +147 -136
- package/templates/modules/docker/docker-compose.yml.hbs +96 -1
- package/templates/modules/drizzle/express/src/lib/schema.ts +3 -1
- package/templates/modules/drizzle/nestjs/src/database/schema.ts +3 -1
- package/templates/modules/drizzle/nextjs/lib/schema.ts.hbs +11 -5
- package/templates/modules/drizzle/tanstack/src/lib/schema.ts.hbs +90 -84
- package/templates/modules/rate-limit/express/config.json +12 -0
- package/templates/modules/rate-limit/express/src/middleware/rateLimit.ts +21 -0
- package/templates/modules/rate-limit/nestjs/config.json +12 -0
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,13 @@ 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 }));
|
|
80
98
|
const initGit = await ask(preset.initGit, () => (0, ui_1.confirm)({ message: "Initialize a git repository?", initialValue: true }));
|
|
81
99
|
const installDeps = await ask(preset.installDeps, () => (0, ui_1.confirm)({ message: "Install dependencies now?", initialValue: false }));
|
|
82
100
|
return {
|
|
@@ -85,6 +103,7 @@ async function collectStackChoices(preset) {
|
|
|
85
103
|
packageManager,
|
|
86
104
|
useDocker,
|
|
87
105
|
useAuth,
|
|
106
|
+
useRateLimit,
|
|
88
107
|
initGit,
|
|
89
108
|
installDeps,
|
|
90
109
|
};
|
package/dist/types/index.js
CHANGED
|
@@ -11,6 +11,7 @@ exports.DEFAULT_CHOICES = {
|
|
|
11
11
|
database: "none",
|
|
12
12
|
useDocker: false,
|
|
13
13
|
useAuth: false,
|
|
14
|
+
useRateLimit: false,
|
|
14
15
|
packageManager: "npm",
|
|
15
16
|
initGit: true,
|
|
16
17
|
installDeps: false,
|
|
@@ -52,7 +53,7 @@ exports.frontendFrameworkOptions = [
|
|
|
52
53
|
{
|
|
53
54
|
value: "tanstack",
|
|
54
55
|
label: "TanStack Start",
|
|
55
|
-
hint: "
|
|
56
|
+
hint: "Vite and React",
|
|
56
57
|
},
|
|
57
58
|
];
|
|
58
59
|
// Database system options; the ORM is chosen in a second step
|
package/dist/ui/messages.js
CHANGED
|
@@ -73,6 +73,7 @@ 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",
|
|
77
78
|
];
|
|
78
79
|
const setup = [
|
|
@@ -115,7 +116,7 @@ function showNextSteps(steps) {
|
|
|
115
116
|
write();
|
|
116
117
|
write(` ${(0, theme_1.amber)((0, theme_1.bold)("Next steps"))}`);
|
|
117
118
|
steps.forEach((step, index) => {
|
|
118
|
-
const isCommand = /^(cd|npm|pnpm|bun) /.test(step);
|
|
119
|
+
const isCommand = /^(cd|npm|pnpm|bun|docker) /.test(step);
|
|
119
120
|
write(` ${(0, theme_1.dim)(`${index + 1}.`)} ${isCommand ? (0, theme_1.lime)(step) : step}`);
|
|
120
121
|
});
|
|
121
122
|
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,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.2.1",
|
|
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.0"
|
|
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.2.1",
|
|
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.0",
|
|
22
|
+
"zod": "^4.6.5"
|
|
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.13.3",
|
|
31
|
+
"eslint": "^10.10.0",
|
|
32
|
+
"eslint-config-prettier": "^10.1.8",
|
|
33
|
+
"prettier": "^3.9.6",
|
|
34
|
+
"tsx": "^4.23.13",
|
|
35
|
+
"typescript": "^6.0.3",
|
|
36
|
+
"typescript-eslint": "^8.69.0"
|
|
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;
|
|
@@ -1,41 +1,43 @@
|
|
|
1
|
-
{
|
|
2
|
-
"id": "nestjs",
|
|
3
|
-
"name": "NestJS Base",
|
|
4
|
-
"description": "Base NestJS server with TypeScript, security headers, and a health check",
|
|
5
|
-
"type": "base",
|
|
6
|
-
"scripts": {
|
|
7
|
-
"dev": "cross-env NODE_ENV=development nest start --watch",
|
|
8
|
-
"build": "nest build",
|
|
9
|
-
"start": "cross-env NODE_ENV=production {{runner}} dist/main.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
|
-
"@nestjs/common": "^12.1.2",
|
|
17
|
-
"@nestjs/core": "^12.1.2",
|
|
18
|
-
"@nestjs/platform-express": "^12.1.2",
|
|
19
|
-
"cross-env": "^10.1.0",
|
|
20
|
-
"dotenv": "^17.4.2",
|
|
21
|
-
"helmet": "^8.3.0",
|
|
22
|
-
"reflect-metadata": "^0.2.2",
|
|
23
|
-
"rxjs": "^7.8.2"
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
"@
|
|
28
|
-
"@nestjs/
|
|
29
|
-
"@
|
|
30
|
-
"@
|
|
31
|
-
"@types/
|
|
32
|
-
"
|
|
33
|
-
"eslint
|
|
34
|
-
"prettier": "^
|
|
35
|
-
"
|
|
36
|
-
"typescript
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
1
|
+
{
|
|
2
|
+
"id": "nestjs",
|
|
3
|
+
"name": "NestJS Base",
|
|
4
|
+
"description": "Base NestJS server with TypeScript, security headers, and a health check",
|
|
5
|
+
"type": "base",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "cross-env NODE_ENV=development nest start --watch",
|
|
8
|
+
"build": "nest build",
|
|
9
|
+
"start": "cross-env NODE_ENV=production {{runner}} dist/main.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
|
+
"@nestjs/common": "^12.1.2",
|
|
17
|
+
"@nestjs/core": "^12.1.2",
|
|
18
|
+
"@nestjs/platform-express": "^12.1.2",
|
|
19
|
+
"cross-env": "^10.1.0",
|
|
20
|
+
"dotenv": "^17.4.2",
|
|
21
|
+
"helmet": "^8.3.0",
|
|
22
|
+
"reflect-metadata": "^0.2.2",
|
|
23
|
+
"rxjs": "^7.8.2",
|
|
24
|
+
"zod": "^4.6.5"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@eslint/js": "^10.0.1",
|
|
28
|
+
"@nestjs/cli": "^12.0.8",
|
|
29
|
+
"@nestjs/schematics": "^12.0.6",
|
|
30
|
+
"@trivago/prettier-plugin-sort-imports": "^6.0.2",
|
|
31
|
+
"@types/express": "^5.0.6",
|
|
32
|
+
"@types/node": "^24.13.3",
|
|
33
|
+
"eslint": "^10.10.0",
|
|
34
|
+
"eslint-config-prettier": "^10.1.8",
|
|
35
|
+
"prettier": "^3.9.6",
|
|
36
|
+
"typescript": "^6.0.3",
|
|
37
|
+
"typescript-eslint": "^8.69.0"
|
|
38
|
+
},
|
|
39
|
+
"envVars": {
|
|
40
|
+
"PORT": "3000",
|
|
41
|
+
"CORS_ORIGIN": "*"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { Module } from "@nestjs/common";
|
|
2
|
+
{{#if useRateLimit}}
|
|
3
|
+
import { APP_GUARD } from "@nestjs/core";
|
|
4
|
+
{{/if}}
|
|
2
5
|
{{#if (eq database 'mongoose')}}
|
|
3
6
|
import { MongooseModule } from "@nestjs/mongoose";
|
|
4
7
|
{{/if}}
|
|
8
|
+
{{#if useRateLimit}}
|
|
9
|
+
import { ThrottlerGuard, ThrottlerModule } from "@nestjs/throttler";
|
|
10
|
+
{{/if}}
|
|
5
11
|
|
|
6
12
|
{{#if useAuth}}
|
|
7
13
|
import { AuthModule } from "./auth/auth.module.js";
|
|
@@ -28,7 +34,14 @@ import { PrismaModule } from "./prisma/prisma.module.js";
|
|
|
28
34
|
{{#if useAuth}}
|
|
29
35
|
AuthModule,
|
|
30
36
|
{{/if}}
|
|
37
|
+
{{#if useRateLimit}}
|
|
38
|
+
// Every route: 100 requests per client IP in 15 minutes
|
|
39
|
+
ThrottlerModule.forRoot([{ ttl: 15 * 60 * 1000, limit: 100 }]),
|
|
40
|
+
{{/if}}
|
|
31
41
|
],
|
|
32
42
|
controllers: [HealthController],
|
|
43
|
+
{{#if useRateLimit}}
|
|
44
|
+
providers: [{ provide: APP_GUARD, useClass: ThrottlerGuard }],
|
|
45
|
+
{{/if}}
|
|
33
46
|
})
|
|
34
47
|
export class AppModule {}
|
|
@@ -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;
|
|
@@ -4,13 +4,14 @@ import "dotenv/config";
|
|
|
4
4
|
import helmet from "helmet";
|
|
5
5
|
|
|
6
6
|
import { AppModule } from "./app.module.js";
|
|
7
|
+
import { env } from "./env.js";
|
|
7
8
|
|
|
8
9
|
async function bootstrap(): Promise<void> {
|
|
9
10
|
const app = await NestFactory.create(AppModule);
|
|
10
|
-
const port =
|
|
11
|
+
const port = env.PORT;
|
|
11
12
|
|
|
12
13
|
app.use(helmet());
|
|
13
|
-
app.enableCors();
|
|
14
|
+
app.enableCors({ origin: env.CORS_ORIGIN });
|
|
14
15
|
// Lets modules close database connections when the process is stopped
|
|
15
16
|
app.enableShutdownHooks();
|
|
16
17
|
|