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.
Files changed (68) hide show
  1. package/README.md +114 -113
  2. package/dist/args.js +27 -20
  3. package/dist/builder/buildPlanner.js +1 -1
  4. package/dist/builder/configGenerator.js +4 -6
  5. package/dist/builder/index.js +15 -3
  6. package/dist/builder/moduleSelector.js +6 -2
  7. package/dist/builder/readmeGenerator.js +107 -0
  8. package/dist/builder/templateContext.js +53 -0
  9. package/dist/index.js +7 -1
  10. package/dist/output/nextSteps.js +23 -22
  11. package/dist/prompts/index.js +5 -2
  12. package/dist/prompts/stack.js +23 -2
  13. package/dist/types/index.js +2 -0
  14. package/dist/ui/messages.js +3 -1
  15. package/dist/validators/index.js +5 -2
  16. package/package.json +3 -3
  17. package/templates/backend/express/config.json +42 -40
  18. package/templates/backend/express/gitignore +3 -0
  19. package/templates/backend/express/src/env.ts.hbs +39 -0
  20. package/templates/backend/express/src/index.ts.hbs +111 -75
  21. package/templates/backend/nestjs/config.json +43 -41
  22. package/templates/backend/nestjs/gitignore +3 -0
  23. package/templates/backend/nestjs/src/app.module.ts.hbs +13 -0
  24. package/templates/backend/nestjs/src/env.ts.hbs +39 -0
  25. package/templates/backend/nestjs/src/health/{health.controller.ts → health.controller.ts.hbs} +8 -0
  26. package/templates/backend/nestjs/src/main.ts +3 -2
  27. package/templates/frontend/nextjs/app/page.tsx.hbs +98 -97
  28. package/templates/frontend/nextjs/config.json +12 -14
  29. package/templates/frontend/nextjs/gitignore +3 -0
  30. package/templates/frontend/tanstack/config.json +16 -16
  31. package/templates/frontend/tanstack/gitignore +3 -0
  32. package/templates/frontend/tanstack/src/routes/index.tsx.hbs +102 -101
  33. package/templates/frontend/tanstack/vite.config.ts +23 -14
  34. package/templates/modules/auth/express/config.json +20 -20
  35. package/templates/modules/auth/express/src/middleware/requireAuth.ts.hbs +1 -2
  36. package/templates/modules/auth/express/src/routes/auth.ts.hbs +27 -19
  37. package/templates/modules/auth/nestjs/config.json +20 -20
  38. package/templates/modules/auth/nestjs/src/auth/{auth.controller.ts → auth.controller.ts.hbs} +15 -0
  39. package/templates/modules/auth/nextjs/config.json +4 -4
  40. package/templates/{frontend/nextjs/proxy.ts.hbs → modules/auth/nextjs/proxy.ts} +1 -8
  41. package/templates/modules/auth/tanstack/config.json +4 -4
  42. package/templates/modules/auth/tanstack/src/components/auth/sign-in-form.tsx +132 -134
  43. package/templates/modules/auth/tanstack/src/components/auth/sign-out-button.tsx +46 -48
  44. package/templates/modules/auth/tanstack/src/components/auth/sign-up-form.tsx +150 -152
  45. package/templates/modules/ci/.github/workflows/ci.yml.hbs +43 -0
  46. package/templates/modules/ci/config.json +10 -0
  47. package/templates/modules/docker/.dockerignore +2 -0
  48. package/templates/modules/docker/Dockerfile.hbs +147 -136
  49. package/templates/modules/docker/docker-compose.yml.hbs +97 -2
  50. package/templates/modules/drizzle/express/config.json +3 -3
  51. package/templates/modules/drizzle/nestjs/config.json +3 -3
  52. package/templates/modules/drizzle/nextjs/config.json +3 -3
  53. package/templates/modules/drizzle/tanstack/config.json +3 -3
  54. package/templates/modules/mongoose/express/config.json +2 -2
  55. package/templates/modules/mongoose/nestjs/config.json +1 -1
  56. package/templates/modules/mongoose/nextjs/config.json +2 -2
  57. package/templates/modules/mongoose/tanstack/config.json +2 -2
  58. package/templates/modules/prisma/express/config.json +4 -4
  59. package/templates/modules/prisma/express/prisma.config.ts +4 -2
  60. package/templates/modules/prisma/nestjs/config.json +4 -4
  61. package/templates/modules/prisma/nestjs/prisma.config.ts +4 -2
  62. package/templates/modules/prisma/nextjs/config.json +4 -4
  63. package/templates/modules/prisma/nextjs/prisma.config.ts +4 -2
  64. package/templates/modules/prisma/tanstack/config.json +4 -4
  65. package/templates/modules/prisma/tanstack/prisma.config.ts +4 -2
  66. package/templates/modules/rate-limit/express/config.json +12 -0
  67. package/templates/modules/rate-limit/express/src/middleware/rateLimit.ts +23 -0
  68. package/templates/modules/rate-limit/nestjs/config.json +12 -0
@@ -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
- // Node 24 releases before 24.20 report mismatched file stats on Windows, which
11
- // makes srvx (TanStack's production server) reject every static asset
12
- const MIN_TANSTACK_WINDOWS_NODE = [24, 20];
13
- function isNodeBelow([major, minor]) {
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
- steps.push("Ensure MongoDB is running locally or update MONGODB_URI in .env");
59
- // Better Auth's MongoDB adapter uses transactions, which need a replica set
60
- if (config.useAuth && isFullstack) {
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
  }
@@ -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
- validate: (value) => (0, validators_1.validateProjectPath)(value ?? ""),
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
  }
@@ -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,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
  };
@@ -11,6 +11,8 @@ exports.DEFAULT_CHOICES = {
11
11
  database: "none",
12
12
  useDocker: false,
13
13
  useAuth: false,
14
+ useRateLimit: false,
15
+ useCi: false,
14
16
  packageManager: "npm",
15
17
  initGit: true,
16
18
  installDeps: false,
@@ -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();
@@ -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.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.9",
55
- "zod": "^4.5.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.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.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
+ }
@@ -34,3 +34,6 @@ coverage/
34
34
 
35
35
  # Misc
36
36
  *.tsbuildinfo
37
+
38
+ # Prisma client, created by prisma generate
39
+ src/generated/
@@ -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;