better-ts-stack 2.0.2 → 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.
Files changed (40) hide show
  1. package/README.md +114 -113
  2. package/dist/args.js +24 -20
  3. package/dist/builder/buildPlanner.js +1 -1
  4. package/dist/builder/configGenerator.js +1 -5
  5. package/dist/builder/index.js +15 -3
  6. package/dist/builder/moduleSelector.js +4 -2
  7. package/dist/builder/readmeGenerator.js +101 -0
  8. package/dist/builder/templateContext.js +30 -0
  9. package/dist/index.js +7 -1
  10. package/dist/output/nextSteps.js +19 -22
  11. package/dist/prompts/index.js +2 -1
  12. package/dist/prompts/stack.js +21 -2
  13. package/dist/types/index.js +1 -0
  14. package/dist/ui/messages.js +2 -1
  15. package/dist/validators/index.js +5 -2
  16. package/package.json +1 -1
  17. package/templates/backend/express/config.json +42 -40
  18. package/templates/backend/express/src/env.ts.hbs +39 -0
  19. package/templates/backend/express/src/index.ts.hbs +111 -75
  20. package/templates/backend/nestjs/config.json +43 -41
  21. package/templates/backend/nestjs/src/app.module.ts.hbs +13 -0
  22. package/templates/backend/nestjs/src/env.ts.hbs +39 -0
  23. package/templates/backend/nestjs/src/main.ts +3 -2
  24. package/templates/frontend/nextjs/app/page.tsx.hbs +98 -97
  25. package/templates/frontend/tanstack/config.json +2 -2
  26. package/templates/frontend/tanstack/src/routes/index.tsx.hbs +102 -101
  27. package/templates/frontend/tanstack/vite.config.ts +22 -14
  28. package/templates/modules/auth/express/config.json +20 -20
  29. package/templates/modules/auth/express/src/routes/auth.ts.hbs +27 -19
  30. package/templates/modules/auth/nestjs/config.json +20 -20
  31. package/templates/modules/auth/nestjs/src/auth/{auth.controller.ts → auth.controller.ts.hbs} +15 -0
  32. package/templates/modules/auth/tanstack/src/components/auth/sign-in-form.tsx +132 -134
  33. package/templates/modules/auth/tanstack/src/components/auth/sign-out-button.tsx +46 -48
  34. package/templates/modules/auth/tanstack/src/components/auth/sign-up-form.tsx +150 -152
  35. package/templates/modules/docker/.dockerignore +1 -0
  36. package/templates/modules/docker/Dockerfile.hbs +147 -136
  37. package/templates/modules/docker/docker-compose.yml.hbs +96 -1
  38. package/templates/modules/rate-limit/express/config.json +12 -0
  39. package/templates/modules/rate-limit/express/src/middleware/rateLimit.ts +21 -0
  40. package/templates/modules/rate-limit/nestjs/config.json +12 -0
@@ -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
- process.exit(0);
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: "npm",
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
  };
@@ -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,
@@ -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();
@@ -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.length === 0)
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.0.2",
3
+ "version": "2.1.0",
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",
@@ -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
- "devDependencies": {
24
- "@eslint/js": "^10.0.1",
25
- "@trivago/prettier-plugin-sort-imports": "^6.0.2",
26
- "@types/cors": "^2.8.19",
27
- "@types/express": "^5.0.6",
28
- "@types/morgan": "^1.9.10",
29
- "@types/node": "^24.13.3",
30
- "eslint": "^10.10.0",
31
- "eslint-config-prettier": "^10.1.8",
32
- "prettier": "^3.9.6",
33
- "tsx": "^4.23.13",
34
- "typescript": "^6.0.3",
35
- "typescript-eslint": "^8.69.0"
36
- },
37
- "envVars": {
38
- "PORT": "3000"
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
- {{#if (eq database 'mongoose')}}
7
- import { connectDB } from "./lib/db";
8
- {{/if}}
9
- import healthRouter from "./routes/health";
10
- {{#if useAuth}}
11
- import authRouter from "./routes/auth";
12
- {{/if}}
13
-
14
- const app: Express = express();
15
- const port = process.env.PORT || 3000;
16
-
17
- // Middleware
18
- app.use(helmet());
19
- app.use(cors());
20
- app.use(morgan("dev"));
21
- app.use(express.json());
22
- app.use(express.urlencoded({ extended: true }));
23
-
24
- // Routes
25
- app.use("/health", healthRouter);
26
- {{#if useAuth}}
27
- app.use("/auth", authRouter);
28
- {{/if}}
29
-
30
- // 404 handler
31
- app.use((_req: Request, res: Response) => {
32
- res.status(404).json({ error: "Not Found" });
33
- });
34
-
35
- // Error handler
36
- app.use(
37
- (
38
- err: Error & { status?: number; statusCode?: number; expose?: boolean },
39
- _req: Request,
40
- res: Response,
41
- _next: NextFunction,
42
- ) => {
43
- // Middleware such as express.json() sets 4xx statuses (bad JSON, body too large)
44
- const status = err.status ?? err.statusCode ?? 500;
45
-
46
- if (status >= 400 && status < 500) {
47
- res.status(status).json({ error: err.expose ? err.message : "Bad Request" });
48
- return;
49
- }
50
-
51
- console.error(err.stack);
52
- res.status(500).json({ error: "Internal Server Error" });
53
- },
54
- );
55
-
56
- function listen(): void {
57
- app.listen(port, () => {
58
- console.log(`Server is running on http://localhost:${port}
59
- Health: http://localhost:${port}/health
60
- `);
61
- });
62
- }
63
-
64
- {{#if (eq database 'mongoose')}}
65
- void connectDB()
66
- .then(listen)
67
- .catch((error: unknown) => {
68
- console.error(error);
69
- process.exitCode = 1;
70
- });
71
- {{else}}
72
- listen();
73
- {{/if}}
74
-
75
- export default app;
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
- "devDependencies": {
26
- "@eslint/js": "^10.0.1",
27
- "@nestjs/cli": "^12.0.8",
28
- "@nestjs/schematics": "^12.0.6",
29
- "@trivago/prettier-plugin-sort-imports": "^6.0.2",
30
- "@types/express": "^5.0.6",
31
- "@types/node": "^24.13.3",
32
- "eslint": "^10.10.0",
33
- "eslint-config-prettier": "^10.1.8",
34
- "prettier": "^3.9.6",
35
- "typescript": "^6.0.3",
36
- "typescript-eslint": "^8.69.0"
37
- },
38
- "envVars": {
39
- "PORT": "3000"
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 = process.env.PORT ?? 3000;
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